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 使用,后续基于它进行事务的操作,并且也得到了预期的结果。

相关推荐
IvorySQL14 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·14 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德14 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫14 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i14 小时前
完全卸载MariaDB
数据库·mariadb
纤纡.15 小时前
Linux中SQL 从基础到进阶:五大分类详解与表结构操作(ALTER/DROP)全攻略
linux·数据库·sql
jiunian_cn15 小时前
【Redis】渐进式遍历
数据库·redis·缓存
橙露15 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot
冰暮流星15 小时前
sql语言之分组语句group by
java·数据库·sql
符哥200815 小时前
Ubuntu 常用指令集大全(附实操实例)
数据库·ubuntu·postgresql