Skip to content

事务 ​

本章目标 ​

  1. 说清什么是事务、为什么需要事务(原子性)。
  2. 会用 dataSource.transaction(async (manager) => {...}) 写自动提交/回滚的事务,并能指定隔离级别。
  3. 会用 QueryRunner 手动控制事务(connect → startTransaction → commit/rollback → release)。
  4. 理解嵌套事务与保存点(SAVEPOINT)的行为。
  5. 避开"事务里用了外部 repository"这个最经典的事务 bug。

核心概念 ​

事务就是"打包售票":银行转账要扣张三的钱、加李四的钱,这两步必须"要么都成功,要么都失败"。如果扣完钱系统崩溃了,李四没收到钱,钱就凭空消失了。事务(Transaction)把多步操作打包成一个整体:中途任何一步失败,整个包作废(回滚 ROLLBACK),数据回到打包前的状态;全部成功才一次性盖章生效(提交 COMMIT)。

TypeORM 提供两种"打包方式":

方式类比适用场景
dataSource.transaction()自动打包机:回调正常结束自动提交,抛异常自动回滚绝大多数业务代码(推荐)
QueryRunner 手动事务手动打包:自己取连接、自己喊开始/提交/回滚/归还需要精细控制、保存点、跨多个步骤判断

自动事务 ​

ts
import { AppDataSource } from "./data-source";
import { User } from "./entities/User";

AppDataSource.initialize()
  .then(async () => {
    // 简单事务:先查询,再修改,要么全部成功提交,要么失败回滚
    await AppDataSource.transaction(async (manager) => {
      // 1. 事务内查询:找到 id 为 1 的用户
      const user = await manager.findOneBy(User, { id: 1 });

      console.log("修改前:", user);

      // 2. 事务内修改:更新用户年龄
      user.age = user.age + 1;
      await manager.save(user);

      console.log("修改后:", user);
      // 如果此处抛出异常,上面的修改会自动回滚
    });

    console.log("事务提交成功");
  })
  .catch((error) => console.log(error));

2. 隔离级别 ​

隔离级别回答一个问题:"多个事务同时跑时,彼此能互相看到多少中间结果?"级别越高越安全但越慢。TypeORM 1.1.1 支持的级别(查 driver/types/IsolationLevel.d.ts):

隔离级别脏读不可重复读幻读说明
READ UNCOMMITTED❌ 可能❌ 可能❌ 可能最宽松,能读到别人未提交的修改
READ COMMITTED✅ 不会❌ 可能❌ 可能只读已提交的数据
REPEATABLE READ✅ 不会✅ 不会❌ 可能(InnoDB 基本避免)MySQL InnoDB 默认级别
SERIALIZABLE✅ 不会✅ 不会✅ 不会最严格,相当于串行执行,并发性能最差

(SNAPSHOT 仅 MSSQL 支持,MySQL 不用。)

ts
await ds.transaction("SERIALIZABLE", async (manager) => {
  // 这个事务运行在 SERIALIZABLE 级别
});

手动事务 ​

QueryRunner 是"一条专用连接的控制器"。手动事务的标准骨架(查 query-runner/QueryRunner.d.ts 第 58、71、82、87、92 行确认方法签名):

ts
const runner = ds.createQueryRunner();
await runner.connect(); // 从连接池取一条专用连接
await runner.startTransaction(); // 可传隔离级别:startTransaction("SERIALIZABLE")
try {
  // 所有操作用 runner.manager(它绑定在这条连接上)
  await runner.manager.save(account);
  await runner.commitTransaction(); // 提交
} catch (e) {
  await runner.rollbackTransaction(); // 回滚
} finally {
  await runner.release(); // 归还连接到池子,必须放 finally!
}

嵌套事务与保存点 SAVEPOINT ​

在同一个 QueryRunner 上,事务进行中再次调用 startTransaction(),TypeORM 不会真开第二个事务,而是自动改用 MySQL 的保存点机制(查 driver/mysql/MysqlQueryRunner.js 源码确认):

  • 第 2 次 startTransaction() → 执行 SAVEPOINT typeorm_1
  • 此时 rollbackTransaction() → ROLLBACK TO SAVEPOINT typeorm_1(只撤销保存点之后的操作)
  • 此时 commitTransaction() → RELEASE SAVEPOINT typeorm_1(保存点内的操作并入外层事务)
  • 最外层的 commitTransaction() 才真正 COMMIT

这就像写文档时的"局部撤销":整篇文档是一个事务,保存点让你可以只撤销最后一段,而不必推翻全文。

ts
await runner.startTransaction(); // 外层 BEGIN
await runner.manager.save(a); // 操作 A
await runner.startTransaction(); // 自动变 SAVEPOINT typeorm_1
try {
  await runner.manager.save(b); // 操作 B
  throw new Error("B 失败");
} catch {
  await runner.rollbackTransaction(); // 只回滚 B,A 保留
}
await runner.commitTransaction(); // 提交 A