前言
数据库的事务是日常开发中的常规操作。数据库操作中如果遇到一个业务涉及到多表操作的,就需要进行事务处理,保持数据的一致性。 本文基于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
使用,后续基于它进行事务的操作,并且也得到了预期的结果。