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

相关推荐
加油=^_^=2 分钟前
MySQL基础篇的补充
数据库·python·mysql
porkczr34 分钟前
oracle rac多个实例就相当于多个数据库系统程序
数据库·oracle
大白菜和MySQL1 小时前
mysql mha高可用集群搭建
数据库·mysql
QQ爱剪辑1 小时前
MySQL基础(13)- MySQL数据类型
数据库·mysql
后端小张2 小时前
Redis 执行 Lua,能保证原子性吗?
数据库·redis·缓存
离开地球表面_992 小时前
索引失效?查询结果不正确?原来都是隐式转换惹的祸
数据库·后端·mysql
lipviolet3 小时前
Redis系列---Redission分布式锁
数据库·redis·分布式
Zhen (Evan) Wang3 小时前
.NET 6 API + Dapper + SQL Server 2014
数据库·c#·.net
毕设木哥3 小时前
25届计算机专业毕设选题推荐-基于python+Django协调过滤的新闻推荐系统
大数据·服务器·数据库·python·django·毕业设计·课程设计
洛阳泰山4 小时前
Llamaindex 使用过程中的常见问题 (FAQ)
java·服务器·数据库·python·llm·rag·llamaindex