事务管理
node-mybatis-plus 提供两种事务管理方式:编程式事务(withTransaction)和声明式事务(@Transactional 装饰器)。两者底层都基于 AsyncLocalStorage 实现事务连接的自动传播,嵌套调用时复用外层事务。
编程式事务 withTransaction
withTransaction 接收一个 DataSource 和一个 async 回调函数。回调正常结束自动 commit,抛出异常自动 rollback:
ts
import { createDataSource, BaseMapper, withTransaction } from 'node-mybatis-plus';
const ds = createDataSource({
type: 'mysql',
host: 'localhost',
port: 3306,
database: 'test',
username: 'root',
password: '******',
});
class UserMapper extends BaseMapper<User> {}
class OrderMapper extends BaseMapper<Order> {}
const userMapper = new UserMapper(User, ds);
const orderMapper = new OrderMapper(Order, ds);
// 正常结束 → 自动 commit
await withTransaction(ds, async () => {
await userMapper.insert({ userName: '张三', age: 20, email: 'zs@test.com' });
await orderMapper.insert({ userId: 1, orderNo: 'ORD-001', amount: 99.9 });
});
// 抛异常 → 自动 rollback,两条插入都不会生效
await withTransaction(ds, async () => {
await userMapper.insert({ userName: '李四', age: 25, email: 'ls@test.com' });
throw new Error('业务校验失败');
// 上面的 insert 会被回滚
});TIP
withTransaction 内部会获取一个数据库连接并执行 BEGIN,回调中所有通过同一 DataSource 执行的 SQL 都会自动使用该事务连接。
@Transactional 装饰器
@Transactional 是方法装饰器,使用前需要先调用 setDefaultDataSource 注册默认数据源:
ts
import { setDefaultDataSource, Transactional } from 'node-mybatis-plus';
// 注册默认数据源(通常在应用启动时调用一次)
setDefaultDataSource(ds);然后在 Service 类的方法上使用 @Transactional():
ts
class UserService {
@Transactional()
async createUser(name: string, age: number, email: string) {
const userId = await userMapper.insert({ userName: name, age, email });
await orderMapper.insert({ userId, orderNo: 'INIT-001', amount: 0 });
// 方法正常结束 → 自动 commit
// 方法抛异常 → 自动 rollback
return userId;
}
}
const userService = new UserService();
await userService.createUser('张三', 20, 'zs@test.com');指定数据源
如果有多个数据源,可以通过 options.datasource 指定:
ts
class ReportService {
@Transactional({ datasource: reportDs })
async generateReport() {
// 使用 reportDs 的事务连接
}
}WARNING
使用 @Transactional 前必须调用 setDefaultDataSource(ds) 或在装饰器参数中传入 datasource,否则会抛出错误。
事务传播机制
node-mybatis-plus 的事务传播基于 AsyncLocalStorage。当嵌套调用带有事务的方法时,内层方法会自动复用外层的事务连接,而不是开启新事务:
ts
class OrderService {
@Transactional()
async createOrder(userId: number, items: OrderItem[]) {
const orderId = await orderMapper.insert({
userId,
orderNo: `ORD-${Date.now()}`,
amount: items.reduce((sum, i) => sum + i.price, 0),
});
// 调用另一个 @Transactional 方法 → 复用当前事务
await this.updateStock(items);
return orderId;
}
@Transactional()
async updateStock(items: OrderItem[]) {
for (const item of items) {
await stockMapper.lambdaUpdate()
.set('quantity', item.currentQty - item.orderQty)
.eq('productId', item.productId)
.execute();
}
// 单独调用 → 独立事务
// 被 createOrder 调用 → 复用外层事务
}
}传播原理
createOrder() 开始
├─ AsyncLocalStorage 存储事务连接
├─ INSERT order → 使用事务连接
├─ updateStock() 开始
│ ├─ 检测到已有事务连接 → 复用,不再 BEGIN
│ └─ UPDATE stock → 使用同一事务连接
└─ 正常结束 → COMMIT(一次性提交所有操作)TIP
事务传播的判断条件是:当前 AsyncLocalStorage 上下文中已存在同一 DataSource 的事务连接。如果是不同的 DataSource,则会开启独立事务。
编程式事务同样支持传播
withTransaction 也支持嵌套传播,行为与 @Transactional 一致:
ts
await withTransaction(ds, async () => {
await userMapper.insert({ userName: '张三', age: 20, email: 'zs@test.com' });
// 嵌套 withTransaction → 复用外层事务
await withTransaction(ds, async () => {
await orderMapper.insert({ userId: 1, orderNo: 'ORD-001', amount: 99.9 });
});
// 两条 insert 在同一个事务中
});错误处理
事务中任何位置抛出异常,整个事务都会回滚:
ts
class PaymentService {
@Transactional()
async pay(orderId: number, amount: number) {
// 1. 扣减余额
await accountMapper.lambdaUpdate()
.set('balance', currentBalance - amount)
.eq('id', accountId)
.execute();
// 2. 创建支付记录
await paymentMapper.insert({ orderId, amount, status: 'paid' });
// 3. 如果这里抛异常,步骤 1 和 2 都会回滚
if (amount > 10000) {
throw new Error('单笔支付不能超过 10000');
}
}
}