事务
本章目标
- 说清什么是事务、为什么需要事务(原子性)。
- 会用
dataSource.transaction(async (manager) => {...})写自动提交/回滚的事务,并能指定隔离级别。 - 会用 QueryRunner 手动控制事务(connect → startTransaction → commit/rollback → release)。
- 理解嵌套事务与保存点(SAVEPOINT)的行为。
- 避开"事务里用了外部 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