Nestjs Typeorm 事务的一次实践

前言

数据库的事务是日常开发中的常规操作。数据库操作中如果遇到一个业务涉及到多表操作的,就需要进行事务处理,保持数据的一致性。 本文基于Nestjs 和 TypeOrm 做了一次实践,希望能给阅读文章的同学一点启发,也希望得到相关的意见和讨论。

回调模式

整个事务逻辑代码被包在回调里,由系统进行commit 和 rollback。基于简单的事务可以这么做

ts 复制代码
import { DataSource } from 'typeorm';

@Injectable()
export class UsersService {
  constructor(private dataSource: DataSource) {}
}

async createMany(users: User[]) {
  await this.dataSource.transaction(async manager => {
    await manager.save<User>(users[0]);
    await manager.save<User>(users[1]);
  });
}

使用QueryRunner

官方推荐这样的写法,事务的每一步都可以由开发者控制。

ts 复制代码
async createMany(users: User[]) {
  const queryRunner = this.dataSource.createQueryRunner();

  await queryRunner.connect();
  await queryRunner.startTransaction(); // 开始事务 可选参数 隔离级别
  try {
    await queryRunner.manager.save(users[0]); // 具体的业务逻辑
    await queryRunner.manager.save(users[1]);

    await queryRunner.commitTransaction(); // 提交事务
  } catch (err) {
    await queryRunner.rollbackTransaction(); // 报错回滚
  } finally {
    await queryRunner.release(); //释放createQueryRunner
  }
}

我们可以把这个写成一个公共的组件,具体思路如下:

  • 一个公共的interceptor ,用于事务的相关操作,把queryRunner.manager 放到对应请求的req 对象上
  • 一个公共的provider ,用于获取request 上的queryRunner.manager
  • 在module 里注入provider,在controller 里注入 interceptor
  • 在service 里 使用 provider 获取的manger 进行操作

interceptor

首先写一下 interceptor

ts 复制代码
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Request } from 'express';
import { Observable, catchError, concatMap, finalize } from 'rxjs';
import { DataSource } from 'typeorm';
import { InjectDataSource } from '@nestjs/typeorm';

export const ENTITY_MANAGER_KEY = 'ENTITY_MANAGER';

@Injectable()
export class TransactionInterceptor implements NestInterceptor {
  constructor(@InjectDataSource() private dataSource: DataSource) { }
  
  async intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Promise<Observable<any>> {
  
    const contextType = context.getType()
    const req = (contextType == "http" ? context.switchToHttp().getRequest<Request>() : context.switchToRpc().getData())
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    req[ENTITY_MANAGER_KEY] = queryRunner.manager;

    const requestId = req.requestId
    console.log("TransactionInterceptor start transaction " + requestId)
    return next.handle().pipe(
    
      concatMap(async (data) => {
      
        console.log("TransactionInterceptor start commit " + requestId)
        await queryRunner.commitTransaction();
        console.log("TransactionInterceptor end commit " + requestId)
        return data;
        
      }),

      catchError(async (e) => {
      
        console.log("TransactionInterceptor start rollback " + requestId)
        await queryRunner.rollbackTransaction();
        console.log("TransactionInterceptor end rollback " + requestId)
   
        let code: number, message: string;
        if (error.response) {
          const response = error.response
          code = response.code;
          message = response.message
        } else {
          message = error.message
          code = -1
        }

        return {
          code: code,
          message: message,
        }
        
      }),

      finalize(async () => {

        console.log("TransactionInterceptor start release " + requestId)
        await queryRunner.release();
        console.log("TransactionInterceptor end release " + requestId)

      }),
    );
  }
}

梳理一下代码逻辑

  • 通过InjectDataSource 获取typeorm 的datasource
  • 创建 QueryRunner 并启动事务
  • 被装饰的controller 或者service 开始执行。执行中无报错会进入 concatMap 进行 commit,如有报错会进入catchError 进行回滚,最终都会进入finalize

provider

下面是provider

ts 复制代码
import { DataSource, EntityManager, Repository } from 'typeorm';
import { ENTITY_MANAGER_KEY } from '@app/common';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { Request } from 'express';
import { REQUEST } from '@nestjs/core';

@Injectable({ scope: Scope.REQUEST })
export class BaseRepository {

  constructor(@InjectDataSource() private dataSource: DataSource,
    @Inject(REQUEST) private readonly request: Request
  ) { }


  getRepository<T>(entityCls: new () => T): Repository<T> {

    let entityManager: EntityManager = this.dataSource.manager;
    if (this.request[ENTITY_MANAGER_KEY]) {
      // http 请求
      entityManager = this.request[ENTITY_MANAGER_KEY]
    } else if (this.request["data"] && this.request["data"][ENTITY_MANAGER_KEY]) { 
      // rpc 请求
      entityManager = this.request["data"][ENTITY_MANAGER_KEY]
    }
    return entityManager.getRepository(entityCls);

  }
}

这个段代码的目的是获取前面在TransactionInterceptor中放到request 里的 manager。 默认给了已有连接的manager。如果发现 request 上含有刚才设置的manager 就替换掉

调用

下面是写下具体调用过程

先是 在controller 里使用装饰器 @UseInterceptors(TransactionInterceptor)

ts 复制代码
@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) { }

  @UseInterceptors(TransactionInterceptor)
  async register(createUserData: CreateUserRequest): Promise<CreateUserResponse> {
	  await this.userService.checkRegister(createUserData);
      const createResult = await this.userService.create(createUserData)
      return {
        code: 0,
        data: createResult,
        message: "OK",
      }
  }
}

接着在service 调用

ts 复制代码
@Injectable()
export class UserService {

  constructor(
    private roleService: RolesService,
    private baseRepository: BaseRepository
  ) { }

  async create(createUserDto: CreateUserDto): Promise<any> {

    let self = this;
    const { nickname, password, mobile } = createUserDto
    const salt = makeSalt();
    const hashPassword = encryptPassword(password, salt)
    const newUser: User = new User()
    newUser.nickname = nickname
    newUser.mobile = mobile
    newUser.password = hashPassword
    newUser.salt = salt
    
    const result: User = await this.baseRepository.getRepository(User).save(newUser)
    const defaultRoles = await this.roleService.getDefaultRole()

    await self.setUserRole({
      userId: result.userId,
      roleIds: [defaultRoles.id.toString()]
    })

    return result

  }

  async setUserRole(userRoleDto: UserRoleDto) {

    let self = this;
    let { userId, roleIds } = userRoleDto
    const createDatas: UserRoles[] = roleIds.map(function (ri) {
      return self.userRoleRepository.create({ userId, roleId: ri });
    })
    return await self.baseRepository.getRepository(UserRoles).save(createDatas)
    
  }

}

梳理一下代码逻辑

  • 由于我们 entityManager 是放在请求里的,我们BaseRepository 需要在每次调用重新获取,也就是 this.baseRepository.getRepository(User) 。
  • 只要我们的 module 注入了 BaseRepository ,就可以在service 当中取到request 里的EntityManager。比如User 模块UserService 里所有方法都可以取到同一个里的EntityManager,而且,User 模块引入了Role 模块,RoleService 里也可以取到 和UserService 同一个的EntityManager。这样就可以不改变代码结构的基础上构建了事务,否则在各个 service 方法里需要增加参数里传递EntityManager ,当然也没有什么是绝对的,看实际需要吧。

测试

下面我们访问接口测试一下

以 query 字段开头的是 typeorm 打印出的log,其余是程序打印出的log

这里是正常的log

shell 复制代码
query: START TRANSACTION

TransactionInterceptor start transaction accec8c6-eafc-4e7d-b75e-c9dfaa1bbe41

query: SELECT ...

getRepository accec8c6-eafc-4e7d-b75e-c9dfaa1bbe41 [class User]

query: INSERT INTO `user` ...

query: SELECT `User` ...

getRepository accec8c6-eafc-4e7d-b75e-c9dfaa1bbe41 [class Role]

query: SELECT `roles` ...

getRepository accec8c6-eafc-4e7d-b75e-c9dfaa1bbe41 [class UserRoles]

query: INSERT INTO `user_roles` ... 

query: SELECT `UserRoles` ...

TransactionInterceptor start commit accec8c6-eafc-4e7d-b75e-c9dfaa1bbe41

query: COMMIT

TransactionInterceptor end commit accec8c6-eafc-4e7d-b75e-c9dfaa1bbe41

TransactionInterceptor start release accec8c6-eafc-4e7d-b75e-c9dfaa1bbe41

TransactionInterceptor end release accec8c6-eafc-4e7d-b75e-c9dfaa1bbe41

我们可以看到整体的流程是 START TRANSACTION -> 多个 INSERT INTO -> COMMIT

我人为制造了一个错误,来验证下代码,看下来确实触发了 interceptor 里的 catchError rollback

shell 复制代码
query: START TRANSACTION

TransactionInterceptor start transaction c445987d-e5e6-46b1-88dc-f9fe425c0221

query: SELECT `user` ...

getRepository c445987d-e5e6-46b1-88dc-f9fe425c0221 [class User]

query: INSERT INTO `user` ...

query: SELECT `User`.` ...

getRepository c445987d-e5e6-46b1-88dc-f9fe425c0221 [class Role]

query: SELECT `roles` ...

getRepository c445987d-e5e6-46b1-88dc-f9fe425c0221 [class UserRoles]

query: INSERT INTO `user_roles` ...

query failed: INSERT INTO `user_roles` ... 

error: Error: Duplicate entry  ...

TransactionInterceptor start rollback c445987d-e5e6-46b1-88dc-f9fe425c0221

query: ROLLBACK

TransactionInterceptor end rollback c445987d-e5e6-46b1-88dc-f9fe425c0221

TransactionInterceptor start release c445987d-e5e6-46b1-88dc-f9fe425c0221

TransactionInterceptor end release c445987d-e5e6-46b1-88dc-f9fe425c0221

这里的整体流程是 START TRANSACTION -> 多个 INSERT INTO -> Error -> ROLLBACK

总结

在本文中,我们着重介绍了使用QueryRunner 方式进行事务控制的方法。用一个独立的TransactionInterceptor 基于request 生成一个 QueryRunner,配合 BaseRepository 使用,后续基于它进行事务的操作,并且也得到了预期的结果。

相关推荐
一只爱撸猫的程序猿33 分钟前
构建一个简单的智能文档问答系统实例
数据库·spring boot·aigc
nanzhuhe1 小时前
sql中group by使用场景
数据库·sql·数据挖掘
消失在人海中1 小时前
oracle sql 语句 优化方法
数据库·sql·oracle
Clang's Blog1 小时前
一键搭建 WordPress + MySQL + phpMyAdmin 环境(支持 PHP 版本选择 & 自定义配置)
数据库·mysql·php·wordpr
zzc9211 小时前
MATLAB仿真生成无线通信网络拓扑推理数据集
开发语言·网络·数据库·人工智能·python·深度学习·matlab
未来之窗软件服务1 小时前
JAVASCRIPT 前端数据库-V1--仙盟数据库架构-—-—仙盟创梦IDE
数据库·数据库架构·仙盟创梦ide·东方仙盟数据库
LjQ20402 小时前
网络爬虫一课一得
开发语言·数据库·python·网络爬虫
烙印6012 小时前
MyBatis原理剖析(二)
java·数据库·mybatis
RestCloud2 小时前
如何通过ETLCloud实现跨系统数据同步?
数据库·数据仓库·mysql·etl·数据处理·数据同步·集成平台
你是狒狒吗2 小时前
TM中,return new TransactionManagerImpl(raf, fc);为什么返回是new了一个新的实例
java·开发语言·数据库