跨模块实现Prisma事务:DDD架构下无客户端传递方案
问题分析
你当前的事务写法失效主要有两个核心原因:
- 控制器里的
prisma.$transaction接收的是已经执行完成的Promise结果(因为你提前await了userService.create和orderService.create),事务根本没包裹住实际的数据库操作。 - 仓储层依赖的是全局
PrismaClient实例,无法动态切换到事务专用的客户端,导致两个操作不在同一个事务上下文里。
下面提供一种贴合DDD分层原则、无需手动传递事务客户端的简洁实现方案:
解决方案:事务上下文+依赖注入
核心思路是通过事务上下文来动态管理当前是否处于事务中,让仓储层自动获取事务客户端或全局客户端,完全不需要修改仓储和服务层的原有接口。
1. 实现事务上下文(支持异步并发)
用Node.js的AsyncLocalStorage来实现线程安全的事务上下文存储,避免静态变量在并发场景下的冲突:
// transaction.context.ts import { PrismaClient } from '@prisma/client'; import { AsyncLocalStorage } from 'async_hooks'; // 创建异步本地存储实例,专门存事务客户端 const transactionStorage = new AsyncLocalStorage<PrismaClient>(); export class TransactionContext { // 在事务上下文内执行回调 static run<T>(txClient: PrismaClient, callback: () => Promise<T>): Promise<T> { return transactionStorage.run(txClient, callback); } // 获取当前上下文的事务客户端(没有则返回undefined) static getClient(): PrismaClient | undefined { return transactionStorage.getStore(); } }
2. 修改仓储层,动态获取客户端
让仓储优先使用上下文里的事务客户端,没有则用全局注入的PrismaClient:
// repository layer import { injectable, inject } from 'inversify'; import { PrismaClient } from '@prisma/client'; import { TransactionContext } from './transaction.context'; @injectable() export class UserRepository{ constructor(@inject(PrismaClient) private readonly globalPrisma: PrismaClient) {} // 封装客户端获取逻辑 private getClient(): PrismaClient { return TransactionContext.getClient() || this.globalPrisma; } save(user: IUser): Promise<User>{ return this.getClient().user.create({data: user}); } } @injectable() export class OrderRepository{ constructor(@inject(PrismaClient) private readonly globalPrisma: PrismaClient) {} private getClient(): PrismaClient { return TransactionContext.getClient() || this.globalPrisma; } save(order: IOrder): Promise<Order>{ return this.getClient().order.create({data: order}); } }
3. 封装事务执行工具
把事务启动、上下文绑定、清理的逻辑封装成工具函数,简化业务层调用:
// transaction.util.ts import { PrismaClient } from '@prisma/client'; import { TransactionContext } from './transaction.context'; // 通用事务执行函数 export async function runInTransaction<T>( globalPrisma: PrismaClient, callback: () => Promise<T> ): Promise<T> { return globalPrisma.$transaction(async (txClient) => { // 在事务回调内绑定上下文,执行业务逻辑 return TransactionContext.run(txClient, callback); }); }
4. 修改控制器,使用事务工具
现在控制器里只需要调用封装好的事务函数,业务层代码完全不用关心事务细节:
// controller layer import { injectable, inject } from 'inversify'; import { PrismaClient } from '@prisma/client'; import { runInTransaction } from './transaction.util'; @injectable() export class UserController{ constructor( @inject(UserService) private readonly userService: UserService, @inject(OrderService) private readonly orderService: OrderService, @inject(PrismaClient) private readonly prisma: PrismaClient ){} async placeOrder( userRequest: CreateUserRequest, orderRequest: CreateOrderRequest ){ return runInTransaction(this.prisma, async () => { // 这里的两个操作会自动加入同一个事务 await this.userService.create(userRequest); await this.orderService.create(orderRequest); }); } }
方案优势
- 完全符合DDD分层:仓储层只负责数据操作,不关心事务逻辑;服务层专注业务,无需处理事务细节
- 零侵入:不需要修改原有服务和仓储的接口,也不用手动传递事务客户端
- 并发安全:基于
AsyncLocalStorage,多个并发事务之间不会互相干扰 - 扩展性强:后续新增的仓储或服务,只要遵循同样的客户端获取逻辑,自动支持事务
内容的提问来源于stack exchange,提问作者mikrowdev
相关产品推荐
相关产品推荐

