You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在仓储模式中结合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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 10:39:30