6月5日 13:57

TypeORM事务基础:三种写法、常见陷阱和NestJS集成

数据库事务保证一组操作要么全部成功、要么全部回滚——转账是最经典的例子:A 扣 100 元和 B 加 100 元必须在同一个事务里,不能只执行一半。TypeORM 提供三种写事务的方式,这篇文章把每种的使用场景和容易踩的坑讲清楚。

为什么需要事务

不用事务会怎样:

typescript
// ❌ 没有事务保护 async function transfer(fromId: number, toId: number, amount: number) { const from = await userRepo.findOne({ where: { id: fromId } }); from.balance -= amount; await userRepo.save(from); // 成功 const to = await userRepo.findOne({ where: { id: toId } }); to.balance += amount; await userRepo.save(to); // 如果这里报错,A 扣了钱但 B 没收到 }

事务保证:要么两步都成功,要么两步都回滚。

方式一:DataSource.transaction()(日常首选)

最简洁的写法——传一个回调,回调里所有操作自动在一个事务里:

typescript
await dataSource.transaction(async (manager) => { const from = await manager.findOne(User, { where: { id: fromId } }); const to = await manager.findOne(User, { where: { id: toId } }); if (from.balance < amount) { throw new Error('余额不足'); // 抛异常 → 自动 rollback } from.balance -= amount; to.balance += amount; await manager.save([from, to]); // 正常返回 → 自动 commit });

最关键的规则:回调里必须用 manager 参数操作数据库,不能用全局的 userRepository

typescript
// ❌ 错误:全局 repository 不在事务里 await dataSource.transaction(async (manager) => { await manager.save(from); // 在事务里 ✅ await userRepository.save(to); // 不在事务里 ❌ });

全局 Repository 拿到的连接不在事务的连接上,它的操作不受事务保护。这是最常见的事务 bug——代码不报错,但数据不一致。

回调正常返回 → commit

typescript
await dataSource.transaction(async (manager) => { await manager.save(user); // 没有 throw → 自动 commit });

回调抛异常 → rollback

typescript
await dataSource.transaction(async (manager) => { await manager.save(user); throw new Error('出错了'); // → 自动 rollback,上面的 save 被撤销 });

不需要手动 commit 或 rollback——TypeORM 自动处理。

方式二:QueryRunner(精细控制)

当你需要手动控制 commit/rollback 时机、设隔离级别、或同一个连接上跑多个事务时:

typescript
const queryRunner = dataSource.createQueryRunner(); await queryRunner.connect(); // 获取数据库连接 await queryRunner.startTransaction(); // 开启事务 try { await queryRunner.manager.save(user1); await queryRunner.manager.save(user2); // 可以在这里做其他判断,决定 commit 还是 rollback if (someCondition) { await queryRunner.commitTransaction(); } else { await queryRunner.rollbackTransaction(); } } catch (err) { await queryRunner.rollbackTransaction(); throw err; } finally { await queryRunner.release(); // 必须释放连接! }

release() 是必须的——QueryRunner 持有数据库连接,不释放会泄漏连接池。finally 块保证即使 commit 失败也能释放。

什么时候用 QueryRunner

  • 需要手动控制 commit 时机(如根据业务逻辑决定)
  • 需要设置隔离级别:queryRunner.startTransaction('SERIALIZABLE')
  • 需要在同一个连接上执行原生 SQL 和 ORM 操作
  • 批量操作需要分批 commit(避免单个大事务锁表太久)

分批 commit 的模式

typescript
const queryRunner = dataSource.createQueryRunner(); await queryRunner.connect(); const users = await queryRunner.manager.find(User, { take: 10000 }); for (let i = 0; i < users.length; i += 1000) { await queryRunner.startTransaction(); try { const batch = users.slice(i, i + 1000); for (const user of batch) { user.processed = true; await queryRunner.manager.save(user); } await queryRunner.commitTransaction(); } catch (err) { await queryRunner.rollbackTransaction(); } } await queryRunner.release();

每 1000 条一个事务,而不是 10000 条一个大事务。大事务持有锁太久会阻塞其他查询。

方式三:NestJS + TypeORM

NestJS 项目里注入 DataSource 用事务:

typescript
import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; @Injectable() export class OrderService { constructor( @InjectDataSource() private dataSource: DataSource, ) {} async createOrder(dto: CreateOrderDto) { return this.dataSource.transaction(async (manager) => { const order = manager.create(Order, dto); await manager.save(order); for (const item of dto.items) { const product = await manager.findOne(Product, { where: { id: item.productId }, }); product.stock -= item.quantity; await manager.save(product); } return order; }); } }

常见陷阱

事务里调用了事务外的方法

typescript
// ❌ 事务里调用了不在事务里的方法 async function updateProfile(userId: number) { await userRepo.update(userId, { name: 'new' }); // 不在事务里 } await dataSource.transaction(async (manager) => { await manager.save(order); await updateProfile(userId); // 这步不在事务保护下! });

解决:把需要事务保护的操作都放在回调里,或者把 manager 传进去:

typescript
async function updateProfile(manager: EntityManager, userId: number) { await manager.update(User, userId, { name: 'new' }); } await dataSource.transaction(async (manager) => { await manager.save(order); await updateProfile(manager, userId); // ✅ 在事务里 });

事务里做了网络请求

typescript
// ❌ 事务里调第三方 API await dataSource.transaction(async (manager) => { await manager.save(order); await fetch('https://payment-gateway/charge', { ... }); // 网络请求可能超时 await manager.save(payment); });

网络请求可能耗时几秒甚至超时——事务期间数据库锁一直持有,其他请求被阻塞。正确做法:先完成数据库事务,再调外部 API,失败时用补偿机制回滚。

忘记 await

typescript
// ❌ 没有 await,事务还没 commit 就返回了 await dataSource.transaction(async (manager) => { manager.save(user); // 没有 await! });

manager.save() 返回 Promise,没有 await 的话,事务 commit 时 save 可能还没执行完。TypeScript 的 async 函数里忘记 await 不会报错——这是个无声的 bug。

事务 vs 批量操作

不是所有操作都需要事务。批量插入、批量更新如果不需要原子性,可以不用事务——更快:

typescript
// 不需要事务:批量插入日志 await logRepo.insert(logs); // 需要事务:转账(必须保证原子性) await dataSource.transaction(async (manager) => { // ... });

判断标准:这些操作是否必须"全部成功或全部失败"?是 → 用事务,否 → 不用。

标签:TypeORM