【NestJs】使用Prisma实现@Transactional装饰器开启事务并且跨Service传递

69 阅读5分钟

前言

在 NestJS 微服务项目中,事务管理一直是个痛点。当业务逻辑跨越多个 Service 方法时,传统的 Prisma 事务写法会让代码变得臃肿,而且事务上下文难以传递。

本文分享一种基于装饰器的事务管理方案,实现一行代码自动开启事务,并且支持跨方法、跨 Service 的事务上下文传递。


传统事务写法的痛点

在 Prisma 中开启事务通常这样写:

this.prisma.$transaction(tx => {
    // 通过 tx 传递使用事务
    tx.user.update(...);
    // 嵌套事务无法直接传递
    tx.$transaction(...); // ❌ 不支持
});

当业务跨方法时,问题更明显:

class TestService {
    @Inject(PrismaService)
    private prisma: PrismaService;

    updateUser() {
        this.prisma.$transaction(tx => {
            // 用户相关操作
            tx.user.update(...);
            tx.user.create(...);
            
            // 调用其他方法,事务无法传递
            this.updateOrder(); // ❌ 需要重新开启事务
        });
    }

    updateOrder() {
        // 这里必须重新开启事务,无法复用外层事务
        this.prisma.$transaction(tx => {
            tx.order.update(...);
        });
    }
}

核心问题:

  1. 事务上下文无法跨方法传递
  2. 重复的 $transaction 调用,代码冗余
  3. 业务代码与事务管理耦合严重

期望的效果

我希望实现这样的写法:

class TestService {
    @Inject(PrismaService)
    private prisma: PrismaService;

    @Transactional() // ✅ 一行装饰器开启事务
    async updateUser() {
        // 直接使用 prisma,无需手动开启事务
        await this.prisma.user.update(...);
        await this.prisma.user.create(...);
        
        // 跨方法调用,事务自动传递
        await this.updateOrder(); // ✅ 共享同一事务
    }

    async updateOrder() {
        // 直接使用 prisma,自动复用外层事务
        await this.prisma.order.update(...);
    }
}

实现方案

技术选型

市面上已有 nestjs-cls + @nestjs-cls/transactional-adapter-prisma 方案,但为了深入理解原理,我选择自己实现。

核心技术:

  • AsyncLocalStorage:Node.js 原生 API,实现异步上下文隔离
  • 装饰器 + 元数据:标记需要事务的方法
  • Proxy:动态代理方法调用

架构设计

┌─────────────────────────────────────────────────────────────┐
│                    调用流程                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  @Transactional()                                           │
│       ↓                                                     │
│  TransactionalInjector(模块初始化时代理方法)                │
│       ↓                                                     │
│  runInTransaction(开启事务,存储到 AsyncLocalStorage)       │
│       ↓                                                     │
│  业务方法执行                                                │
│       ↓                                                     │
│  PrismaService.current(从上下文获取事务实例)                │
│       ↓                                                     │
│  事务提交/回滚                                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

核心实现

1. 事务上下文管理

使用 AsyncLocalStorage 存储事务实例,这是整个方案的核心:

// transaction/transaction-context.ts
import { AsyncLocalStorage } from 'node:async_hooks';
import { PrismaClient, Prisma } from '@prisma/client';

type PrismaTx = Prisma.TransactionClient; // Prisma 事务客户端类型

// 事务配置选项
export type TransactionOptions = {
  maxWait?: number;           // 最大等待时间
  timeout?: number;           // 超时时间
  isolationLevel?: Prisma.TransactionIsolationLevel; // 隔离级别
};

// 创建异步上下文存储,保存事务实例
const storage = new AsyncLocalStorage<{ tx: PrismaTx }>();

/**
 * 在事务上下文中执行代码
 * @param prisma PrismaClient 实例
 * @param fn 要执行的业务逻辑
 * @param options 事务配置
 */
export async function runInTransaction<T>(
  prisma: PrismaClient,
  fn: () => Promise<T>,
  options?: TransactionOptions,
): Promise<T> {
  return prisma.$transaction(async (tx) => {
    // 将事务实例存入上下文,fn 中可通过 getCurrentTx 获取
    return storage.run({ tx }, fn);
  }, options);
}

/**
 * 获取当前事务上下文中的事务实例
 * @returns 事务实例,如果不在事务中则返回 null
 */
export function getCurrentTx(): PrismaTx | null {
  const store = storage.getStore();
  return store?.tx ?? null;
}

原理说明:

  • AsyncLocalStorage 类似 Java 的 ThreadLocal,但适用于 Node.js 的异步环境
  • storage.run({ tx }, fn) 会将 { tx } 绑定到 fn 的整个异步调用链
  • fn 内部的任何异步调用中,都能通过 storage.getStore() 获取到事务实例

2. 装饰器定义

使用 NestJS 的元数据机制标记需要事务的方法:

// transaction/transactional.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { TransactionOptions } from './transaction-context';

export const TRANSACTIONAL_KEY = 'transactional';

/**
 * @Transactional 装饰器
 * 标记方法需要在事务中执行
 * 
 * @example
 * @Transactional() // 默认配置
 * @Transactional({ timeout: 5000 }) // 自定义超时
 */
export const Transactional = (options: TransactionOptions = {}) =>
  SetMetadata(TRANSACTIONAL_KEY, options);

3. 事务注入器(核心)

这是最关键的部分。传统方案使用 Interceptor 拦截 HTTP 请求,但事务不应该依赖 HTTP 上下文。这里采用 Module Init + Proxy 方式,在模块初始化时扫描并代理所有带装饰器的方法。

这种实现方式学习自开源项目 AiToEarn

// transaction/transactional.injector.ts
import type { Injectable } from '@nestjs/common/interfaces';
import type { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { Injectable as InjectableDec, Logger, OnModuleInit } from '@nestjs/common';
import { MetadataScanner, ModulesContainer } from '@nestjs/core';
import { runInTransaction, TransactionOptions } from './transaction-context';
import { TRANSACTIONAL_KEY } from './transactional.decorator';
import { PrismaService } from '../prisma.service';

/**
 * 事务注入器
 * 在模块初始化时扫描所有带 @Transactional 装饰器的方法,
 * 并为这些方法注入事务处理逻辑
 */
@InjectableDec()
export class TransactionalInjector implements OnModuleInit {
  private readonly logger = new Logger(TransactionalInjector.name);
  private readonly metadataScanner: MetadataScanner = new MetadataScanner();

  constructor(
    private readonly prisma: PrismaService,
    private readonly modulesContainer: ModulesContainer,
  ) {}

  async onModuleInit() {
    // 扫描所有 Provider,注入事务逻辑
    for (const provider of this.getProviders()) {
      this.injectToProvider(provider);
    }
  }

  /**
   * 获取所有 Provider
   */
  private* getProviders(): Generator<InstanceWrapper<Injectable>> {
    for (const module of this.modulesContainer.values()) {
      for (const provider of module.providers.values()) {
        if (provider && provider.metatype?.prototype) {
          yield provider as InstanceWrapper<Injectable>;
        }
      }
    }
  }

  /**
   * 为 Provider 注入事务逻辑
   */
  private injectToProvider(wrapper: InstanceWrapper<Injectable>): void {
    const { metatype } = wrapper;
    if (!metatype) return;

    const prototype = metatype.prototype;
    const methodNames = this.metadataScanner.getAllMethodNames(prototype);

    for (const methodName of methodNames) {
      const method = prototype[methodName];
      
      if (this.isDecorated(method)) {
        const options = this.getDecoratorOptions(method);
        const wrappedMethod = this.wrapMethod(method, methodName, prototype.constructor.name, options);
        this.reDecorate(method, wrappedMethod);
        prototype[methodName] = wrappedMethod;
        
        this.logger.log(`Injected transaction to ${prototype.constructor.name}.${methodName}`);
      }
    }
  }

  private isDecorated(target: object): boolean {
    return Reflect.hasMetadata(TRANSACTIONAL_KEY, target);
  }

  private getDecoratorOptions(target: object): TransactionOptions {
    return Reflect.getMetadata(TRANSACTIONAL_KEY, target);
  }

  /**
   * 保留原有装饰器
   */
  private reDecorate(source: object, destination: object): void {
    const keys = Reflect.getMetadataKeys(source);
    for (const key of keys) {
      const meta = Reflect.getMetadata(key, source);
      Reflect.defineMetadata(key, meta, destination);
    }
  }

  /**
   * 使用 Proxy 包装方法,注入事务逻辑
   */
  private wrapMethod(
    originalMethod: (...args: unknown[]) => unknown,
    methodName: string,
    className: string,
    options: TransactionOptions,
  ): (...args: unknown[]) => unknown {
    return new Proxy(originalMethod, {
      apply: async (target, thisArg, args: unknown[]) => {
        this.logger.debug(`Executing transactional method: ${className}.${methodName}`);
        
        return runInTransaction(this.prisma, async () => {
          return Reflect.apply(target, thisArg, args) as Promise<unknown>;
        }, options);
      },
    });
  }
}

实现亮点:

  1. ModulesContainer 获取 NestJS 容器中的所有模块
  2. MetadataScanner 扫描类的所有方法
  3. Proxy 代理方法调用,自动包裹事务逻辑
  4. 支持定时任务、消息队列等非 HTTP 场景

4. 修改 PrismaService

this.prisma 自动感知事务上下文:

// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { getCurrentTx } from './transaction/transaction-context';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }

  /**
   * 获取当前 PrismaClient 实例
   * 如果在事务中,返回事务实例;否则返回普通实例
   */
  get current() {
    const tx = getCurrentTx();
    
    if (!tx) return this;

    // 使用 Proxy 让调用自动转发到事务实例
    return new Proxy(this, {
      get(target, prop: string | symbol, receiver) {
        // 如果事务实例有该属性/方法,优先使用
        if (prop in tx) {
          const value = (tx as any)[prop];
          if (typeof value === 'function') {
            return value.bind(tx);
          }
          return value;
        }
        // 否则回退到原始 PrismaClient(如 $connect、$on 等)
        return Reflect.get(target, prop, receiver);
      },
    }) as unknown as Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'>;
  }
}

使用方式:

// 在 Service 中
this.prisma.user.findMany(); // 自动使用事务实例(如果存在)

注意:直接使用 this.prisma 调用即可,current getter 会自动返回正确的事务实例或普通实例。


5. 模块注册

// prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { TransactionalInjector } from './transaction/transactional.injector';

@Global()
@Module({
  providers: [PrismaService, TransactionalInjector],
  exports: [PrismaService],
})
export class PrismaModule {}

使用示例

@Injectable()
export class UserService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly orderService: OrderService,
  ) {}

  @Transactional({ timeout: 5000 }) // 5 秒超时
  async purchaseProduct(userId: number, productId: number, amount: number) {
    // 1. 扣减用户积分
    await this.prisma.user.update({
      where: { id: userId },
      data: { integral: { decrement: amount } },
    });

    // 2. 创建订单(跨 Service,共享事务)
    await this.orderService.createOrder(userId, productId, amount);

    // 3. 扣减库存
    await this.prisma.product.update({
      where: { id: productId },
      data: { stock: { decrement: 1 } },
    });

    // 如果任何一步失败,整个事务自动回滚
  }
}

@Injectable()
export class OrderService {
  constructor(private readonly prisma: PrismaService) {}

  async createOrder(userId: number, productId: number, amount: number) {
    // 无需 @Transactional,自动复用外层事务
    return this.prisma.order.create({
      data: { userId, productId, amount },
    });
  }
}

验证事务生效

通过设置极短的超时时间验证:

@Transactional({
  maxWait: 10,  // 最大等待 10ms
  timeout: 10,  // 执行超时 10ms
})
async test() {
  await this.prisma.user.findMany();
}

事务超时报错

看到 Prisma 事务超时错误,说明事务已正确开启。


方案对比

特性传统 $transactionInterceptor 方案Injector 方案(本文)
跨方法传递
非 HTTP 场景
代码侵入性
学习成本
灵活性

总结

本文实现了基于装饰器的 Prisma 事务管理方案,核心思路:

  1. AsyncLocalStorage 实现事务上下文的异步传递
  2. 装饰器 + 元数据 标记需要事务的方法
  3. Module Init + Proxy 在模块初始化时代理方法,注入事务逻辑
  4. PrismaService.current 自动感知事务上下文

这样实现了:

  • ✅ 一行装饰器开启事务
  • ✅ 跨方法、跨 Service 事务传递
  • ✅ 支持定时任务、消息队列等非 HTTP 场景
  • ✅ 业务代码与事务管理解耦

参考