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

在NestJs中我们要开启Prisma事务要这样

ts 复制代码
    this.prisma.$transaction(tx=>{
        //通过tx传递使用事务 
        tx.user.update ... 
        //当我这样调用transaction还无法进行事务传递 当然这个可以通过Proxy代理实现 这里就不做演示了
        ts.$transaction
    })

传统的事务开启方式非常复杂,而且当我们跨方法时需要重复的去开启事务,导致数据库事务开关导致一定的消耗,并且业务代码看的也不是很舒服。

typescript 复制代码
    class TestService {
        @Inject(prisma)
        private prisma:PrismaService
        updateUser(){
           this.prisma.$transaction(tx=>{
               //开启事务
               //...模拟user中进行了很多操作
               tx.user.update...
               tx.user.create...
               //接着需要在order中执行操作
               this.updateOrder()
           
           })
        }
        
        updateOrder(){
            //order执行操作必须得重新开启事务
            this.prisma.$transaction(tx=>{
                tx.order.update ...
            })
        }
    }
    

从代码中可以看出我们在跨方法或者跨Service中去执行事务数据库操作需要通过Prisma重复的去开启事务。 我现在期望这样实现事务

typescript 复制代码
    class TestService {
        @Inject(prisma)
        private prisma:PrismaService
        
        @Transaction() //加入@Transaction()装饰器一键开启事务 并且可跨服务传递
        updateUser(){
               //无需手动开启事务 直接使用prisma实例
               this.prisma.user.update...
               this.prisma.user.create...
               //接着需要在order中执行操作
               this.updateOrder()

        }
        
        updateOrder(){
            //直接调用prisma执行
            tthis.prisma.order.update ...
        }
    }
    

如何实现呢?

最简单的方式是可以通过第三方包nestjs-cls + @nestjs-cls/transactional-adapter-prisma 实现,这里通过第三方包实现就不做讲解了 有兴趣可以自己去npm上查看,秉持着学习的心态我想自己去了解如何实现这样的功能。

首先要使用到AsyncLocalStorage 是 Node.js 提供的重要的异步上下文管理工具,AsyncLocalStorage 让上下文像"线程本地变量"(ThreadLocal)一样跟随异步执行上下文自动传播。 让我们出创建第一个文件

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

type PrismaTx = Prisma.TransactionClient;//定义类型
export type TransactionOptions = { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel };
const storage = new AsyncLocalStorage<{ tx: PrismaTx }>();//创建context 保存事务tx的prisma实例

/**
 * 在事务context中执行代码
 */
export async function runInTransaction<T>(
  prisma: PrismaClient,
  fn: () => Promise<T>,
  options?: TransactionOptions,
): Promise<T> {
  return prisma.$transaction(async (tx) => {
    return storage.run({ tx }, fn);
  }, options);
}

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

创建装饰器

typescript 复制代码
// transaction/transactional.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { TransactionOptions } from './transaction-context';

export const TRANSACTIONAL_KEY = 'transactional';

export const Transactional = (options: TransactionOptions = {}) =>
  SetMetadata(TRANSACTIONAL_KEY, options);

接下来肯定是要通过拦截器去拦截请求,然后通过Reflect去获取元数据拿到事务的options,最后再通过runInTransaction这个方法去调用就可以了

Interceptor拦截器实现

typescript 复制代码
// transaction/transaction.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable, from } from 'rxjs';
import { TRANSACTIONAL_KEY } from './transactional.decorator';
import { PrismaService } from '../../prisma/prisma.service'; // 调整路径
import { runInTransaction } from './transaction-context';

@Injectable()
export class TransactionInterceptor implements NestInterceptor {
  constructor(
    private readonly reflector: Reflector,
    private readonly prisma: PrismaService,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const options = this.reflector.getAllAndOverride<Prisma.TransactionOptions | true>(
      TRANSACTIONAL_KEY,
      [context.getHandler(), context.getClass()],
    );

    // 没有装饰器 → 直接放行
    if (!options) {
      return next.handle();
    }

    const txOptions = options === true ? undefined : options;

    // 用 runInTransaction 包裹整个方法
    const promise = runInTransaction(this.prisma, async () => {
      return next.handle().toPromise?.() ?? next.handle();
    }, txOptions);

    return from(promise);
  }
}

传统的拦截器通过在请求响应的生命周期中获取到元数据执行了我们需要的transaction逻辑,但是我们这个事务的执行本身并不强行依赖HTTP上下文,如果在未开启@Transactional()的情况下依旧会重复的在HTTP生命周期中请求这个拦截器。

Prisma事务开启本身是业务方法级别的需求,且不限于HTTP接口,也可能是定时任务或者消息队列的消费者等等,所以这里我学习到了另外一种Injector的方法,采用的是Module Init + Proxy 的方式实现,来自于一个开源项目:github.com/yikart/AiTo... 接下来我们完成transaction.injector.ts的实现吧

Injector实现

typescript 复制代码
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() {
    for (const provider of this.getProviders()) {
      this.injectToProvider(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>
        }
      }
    }
  }

  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}`)
      }
    }
  }

  /**
   * 判断方法是否携带了Transactional装饰器
   * @param target 方法
   * @returns 是否携带了Transactional装饰器
   */
  private isDecorated(target: object): boolean { 
    return Reflect.hasMetadata(TRANSACTIONAL_KEY, target)
  }
  
  /**
   * 获取携带的Transactional的options
   * @param target 方法
   * @returns Transactional的options
   */
  private getDecoratorOptions(target: object): TransactionOptions {
    return Reflect.getMetadata(TRANSACTIONAL_KEY, target)
  }

  /**
   * 重新装饰方法
   * @param source 源方法
   * @param destination 目标方法
   */
  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)
    }
  }

  /**
   * 包装方法
   * @param originalMethod 原方法
   * @param methodName 方法名
   * @param className 类名
   * @param options 选项
   * @returns 包装后的方法
   */
  private wrapMethod(
    originalMethod: (...args: unknown[]) => unknown,
    methodName: string,
    className: string,
    options: TransactionOptions,
  ): (...args: unknown[]) => unknown {
    // 创建一个代理对象,用于包装原始方法,实现对方法的拦截和增强
    return new Proxy(originalMethod, {
      // 定义代理对象的行为,特别是 apply 方法,用于拦截对原始方法的调用
      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);
      },
    })
  }
}

最后我们修改PrismaService中的get current方法,让在调用this.prisma的时候返回正确实例。

typescript 复制代码
import { OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from 'prismaClients';
import { getCurrentTx } from './transaction/transaction-context';

export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
    async onModuleInit() {
        console.log(`Prisma连接配置: DATABASE_URL=${process.env.DATABASE_URL}`);
        await this.$connect();
    }
    
    async onModuleDestroy() {
        await this.$disconnect();
    }

    /**
     * 修改获取当前PrismaClient实例方法,支持事务。
     */
    get current(){
        const tx = getCurrentTx();
        if(!tx)return this;
        // 返回一个 Proxy,让调用自动转发到 tx
        return new Proxy(this, {
            get(target, prop: string | symbol, receiver) {
                // 如果 tx 有这个方法/属性,优先使用 tx 的(绑定 this 为 tx)
                if (prop in tx) {
                    const value = (tx as any)[prop];
                    if (typeof value === 'function') {
                        return value.bind(tx);
                    }
                    return value;
                }
                // 否则 fallback 到原始 PrismaClient($connect、$on 等)
                return Reflect.get(target, prop, receiver);
            },
            }) as unknown as Omit<PrismaClient, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends">;
        }
    }

到这里整体的代码就编写完成了 接下来在PrismaModule中正常导入注册Providers

typescript 复制代码
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 {}

测试代码

typescript 复制代码
@Injectable()
class UserService{
    @Injecet(PrismaSerivce)
    private prisma:PrismaService;
    
    @Transactional({
        maxWait:10,//这里我们设置10ms超时
        timeout:10
    })
    updateUser(){
        this.prisma.user.update... //调用更新user方法
    }
}

通过上述代码测试得到了报错信息 也就是Prisma的事务执行超时报错 也证明我们的事务已经正常开启了

相关推荐
前端付豪3 天前
Express 如何使用 multer 文件上传?
前端·node.js·nestjs
ArkPppp4 天前
NestJS全栈实战笔记:优雅处理 Entity 与 DTO 的映射与字段过滤
javascript·nestjs
weixin_425543737 天前
TRAE CN3.3.25 构建的Electron简易DEMO应用
前端·typescript·electron·vite·nestjs
亮子AI1 个月前
【NestJS】为什么return不返回客户端?
前端·javascript·git·nestjs
小p1 个月前
nestjs学习2:利用typescript改写express服务
nestjs
Eric_见嘉1 个月前
NestJS 🧑‍🍳 厨子必修课(九):API 文档 Swagger
前端·后端·nestjs
XiaoYu20021 个月前
第3章 Nest.js拦截器
前端·ai编程·nestjs
XiaoYu20021 个月前
第2章 Nest.js入门
前端·ai编程·nestjs
实习生小黄1 个月前
NestJS 调试方案
后端·nestjs