如何在仓储模式中结合Prisma事务实现多表原子操作并适配内存仓储
如何在仓储模式中结合Prisma事务实现多表原子操作并适配内存仓储
嘿,这个场景我太熟了!之前做OAuth用户登录初始化流程的时候,也纠结过怎么在仓储模式下用Prisma事务保证原子性,还要兼容内存仓储做单元测试。刚好可以给你捋捋具体的实现思路,保证业务逻辑完全和底层解耦,还能实现多表操作的原子性。
核心思路:抽象事务上下文,隔离底层实现
咱们的核心目标是让业务用例层完全不知道底层是Prisma还是内存仓储,同时所有表的创建操作必须在同一个事务里执行,失败就全回滚。关键就是用一个抽象的事务上下文把事务状态传递给各个仓储,而不是让仓储自己管理数据库连接或事务。
1. 先定义通用的事务上下文类型
首先搞一个通用的事务上下文接口,让Prisma和内存仓储的实现都能基于它扩展,这样用例层只需要认这个通用类型就行:
// 通用事务上下文,只做标记,具体属性由底层实现填充 type TransactionContext = Record<string, unknown>; // Prisma专属的上下文,包装它的事务客户端 type PrismaTxContext = TransactionContext & { prisma: Prisma.TransactionClient; }; // 内存仓储专属的上下文,用队列存操作和回滚逻辑 type InMemoryTxContext = TransactionContext & { operations: Array<() => Promise<void>>; rollbacks: Array<() => Promise<void>>; };
2. 改造仓储接口,让方法接受事务上下文
原来的仓储创建方法可能直接在内部用全局Prisma客户端,现在得改成把事务上下文传进去,让每个操作都绑定到当前事务:
用户仓储接口
interface IUserRepository { create(context: TransactionContext, data: { email: string; username: string; name: string; }): Promise<{ id: string }>; }
账户仓储接口
interface IAccountRepository { create(context: TransactionContext, data: { user_id: string; provider: string; provider_account_id: string; access_token: string; token_type: string; type: string; }): Promise<void>; }
会话仓储接口
interface ISessionRepository { create(context: TransactionContext, data: { user_id: string; expires: Date; session_token: string; }): Promise<void>; }
3. 实现Prisma版本的仓储
针对Prisma的仓储实现,我们只需要把通用上下文转换成Prisma专属的,然后用里面的事务客户端执行操作就行:
class PrismaUserRepository implements IUserRepository { async create(context: TransactionContext, data: any) { const prismaCtx = context as PrismaTxContext; return prismaCtx.prisma.user.create({ data }); } } class PrismaAccountRepository implements IAccountRepository { async create(context: TransactionContext, data: any) { const prismaCtx = context as PrismaTxContext; await prismaCtx.prisma.account.create({ data }); } } class PrismaSessionRepository implements ISessionRepository { async create(context: TransactionContext, data: any) { const prismaCtx = context as PrismaTxContext; await prismaCtx.prisma.session.create({ data }); } }
4. 实现内存版本的仓储(用于测试)
内存仓储的原子性怎么保证?咱们可以用队列先把所有操作和对应的回滚逻辑存起来,最后统一提交或回滚:
class InMemoryUserRepository implements IUserRepository { private users: Array<{ id: string; email: string; username: string; name: string }> = []; async create(context: TransactionContext, data: any) { const inMemoryCtx = context as InMemoryTxContext; const newUser = { id: Date.now().toString(), ...data }; // 存创建操作,提交时执行 inMemoryCtx.operations.push(async () => this.users.push(newUser)); // 存回滚逻辑,失败时删除这个用户 inMemoryCtx.rollbacks.push(async () => { this.users = this.users.filter(u => u.id !== newUser.id); }); return newUser; } } // 账户和会话的内存仓储实现逻辑完全类似,都是存操作和回滚逻辑到上下文队列
5. 搞一个事务管理器,统一处理事务生命周期
这一步是关键!我们需要一个事务管理器,负责创建上下文、执行业务逻辑、提交/回滚,让用例层完全不用关心底层是Prisma还是内存:
interface ITransactionManager { runInTransaction<T>(callback: (ctx: TransactionContext) => Promise<T>): Promise<T>; }
Prisma事务管理器实现
直接套Prisma自带的$transaction方法,把事务客户端包装成上下文传给业务逻辑:
class PrismaTransactionManager implements ITransactionManager { constructor(private readonly prisma: PrismaClient) {} async runInTransaction<T>(callback: (ctx: TransactionContext) => Promise<T>) { return this.prisma.$transaction(async (prismaTx) => { const ctx: PrismaTxContext = { prisma: prismaTx }; return callback(ctx); }); } }
内存事务管理器实现
先收集所有操作,业务逻辑执行成功就统一提交,失败就执行回滚:
class InMemoryTransactionManager implements ITransactionManager { async runInTransaction<T>(callback: (ctx: TransactionContext) => Promise<T>) { const ctx: InMemoryTxContext = { operations: [], rollbacks: [] }; let result: T; try { // 先执行业务逻辑,收集所有操作 result = await callback(ctx); // 提交:执行所有操作 for (const op of ctx.operations) { await op(); } return result; } catch (error) { // 回滚:执行所有回滚逻辑 for (const rollback of ctx.rollbacks) { await rollback(); } throw error; } } }
6. 业务用例层的最终使用方式
现在业务逻辑完全和底层解耦了,只需要依赖抽象的事务管理器和仓储接口:
class OAuthLoginUseCase { constructor( private readonly txManager: ITransactionManager, private readonly userRepo: IUserRepository, private readonly accountRepo: IAccountRepository, private readonly sessionRepo: ISessionRepository ) {} async execute(input: { email: string; username: string; name: string; provider: string; oauthUserId: string; accessToken: string; tokenType: string; sessionExpires: Date; sessionToken: string; }) { await this.txManager.runInTransaction(async (ctx) => { // 创建用户 const newUser = await this.userRepo.create(ctx, { email: input.email, username: input.username, name: input.name }); // 创建账户 await this.accountRepo.create(ctx, { user_id: newUser.id, provider: input.provider, provider_account_id: input.oauthUserId, access_token: input.accessToken, token_type: input.tokenType, type: 'oauth' }); // 创建会话 await this.sessionRepo.create(ctx, { user_id: newUser.id, expires: input.sessionExpires, session_token: input.sessionToken }); }); } }
最后再划几个重点
- 所有操作的原子性由事务管理器保证,Prisma靠自身的事务,内存仓储靠操作队列的统一提交/回滚
- 业务用例层完全不知道底层是Prisma还是内存仓储,符合依赖倒置原则,换实现只需要换注入的实例
- 每个仓储只负责自己的表,事务逻辑由管理器统一处理,符合单一职责
内容来源于stack exchange
相关产品推荐
相关产品推荐

