事务
事务中的所有查询均在同一数据库连接上执行,并将整个查询集作为一个原子单元运行。任何操作失败都会导致数据库回滚该连接上的所有查询,恢复至事务前的状态。
transaction
在 Orchid ORM 中,事务方法为 $transaction(单独使用 pqb 时为 transaction)。
回调函数成功解析后会自动执行 COMMIT,若回调失败则自动执行 ROLLBACK。
转账场景示例:
export const transferMoney = async (
fromId: number,
toId: number,
amount: number,
) => {
try {
// 事务回调返回值可直接获取
const senderRemainder = await db.$transaction(async () => {
const sender = await db.user.find(fromId);
const newBalance = sender.balance - amount;
if (newBalance < 0) throw new Error('余额不足');
await db.user.find(fromId).decrement({ balance: amount });
await db.user.find(toId).increment({ balance: amount });
return newBalance; // 事务成功后返回
});
} catch (error) {
// 统一处理事务内错误
}
};
该事务包含 3 个操作:查询发送者、扣减余额、增加接收者余额。若任一操作失败(如记录不存在或余额不足),事务将回滚,确保数据一致性。
ORM 内部通过 Node.js 的 AsyncLocalStorage 隐式传递事务对象,回调内的所有查询自动加入事务。
嵌套事务
事务支持嵌套调用:
- 顶级事务:真实数据库事务,通过
BEGIN/COMMIT管理。 - 嵌套事务:通过 Postgres savepoint 模拟,使用
SAVEPOINT/ROLLBACK TO SAVEPOINT实现。
示例:
const result = await db.$transaction(async () => {
await db.table.create({ id: 1 }); // 外部事务操作
const innerResult = await db.$transaction(async () => {
await db.table.create({ id: 2 }); // 嵌套事务操作
if (Math.random() > 0.5) throw new Error('模拟错误');
return 123;
}).catch(err => {
// 捕获嵌套事务错误,执行回滚到保存点
console.log('内部事务回滚');
return null;
});
await db.table.create({ id: 3 }); // 外部事务继续执行
return innerResult; // 内部事务成功时返回 123,失败时返回 null
});
- 嵌套事务抛出未捕获错误时,所有嵌套操作回滚,外部事务可选择继续或终止。
- 外部事务提交时,会自动释放所有保存点并执行最终
COMMIT。
ensureTransaction
当需要确保查询在事务中执行但无需保存点时,使用 $ensureTransaction:
async function updateUserBalance(userId: string, amount: number) {
await db.$ensureTransaction(async trx => { // 自动创建顶级事务
await db.transfer.create({ userId, amount });
await db.user.find(userId).increment({ balance: amount });
});
}
async function saveDeposit(userId: string, deposit: Deposit) {
await db.$ensureTransaction(async () => {
await db.deposit.create(deposit);
await updateUserBalance(userId, deposit.amount); // 共享同一事务
});
}
isInTransaction
返回布尔值判断当前是否处于事务中(测试事务除外):
console.log(db.$isInTransaction()); // false
await db.$transaction(() => {
console.log(db.$isInTransaction()); // true
});
testTransaction
专为测试设计的事务工具,所有操作在内存中执行并在测试后回滚,保持数据库状态清洁:
测试工具配置(src/lib/test-utils.ts):
import { testTransaction } from 'orchid-orm';
import { db } from './db';
export const useTestDatabase = () => {
beforeAll(() => testTransaction.start(db)); // 启动顶级事务
beforeEach(() => testTransaction.start(db)); // 每个测试用例前创建保存点
afterEach(() => testTransaction.rollback(db)); // 回滚到最近保存点
afterAll(() => testTransaction.close(db)); // 关闭事务连接
};
测试用例示例:
import { useTestDatabase } from './test-utils';
describe('用户创建测试', () => {
useTestDatabase(); // 应用测试事务钩子
it('应创建用户记录', async () => {
await db.user.create({ name: 'test' });
expect(await db.user.count()).toBe(1);
});
it('嵌套事务不影响外部状态', async () => {
await db.$transaction(() => db.user.create({ name: 'inner' }));
expect(await db.user.count()).toBe(1); // 嵌套事务内数据可见
});
afterEach(() => {
// 测试后自动回滚,下一个测试用例开始时数据清零
});
});
isolation level
默认事务隔离级别为 SERIALIZABLE(最高一致性),可显式指定其他级别(嵌套事务忽略此设置):
await db.$transaction('READ COMMITTED', async () => {
// 在此级别下执行查询
});
read only, deferrable
设置事务为只读或延迟约束检查(嵌套事务忽略此设置):
await db.$transaction({
readOnly: true, // 只读事务
deferrable: true, // 延迟约束检查
}, async () => {
// 仅允许 SELECT 操作
});
锁控制方法
forUpdate
为查询添加 FOR UPDATE 锁(行级排他锁):
await db.$transaction(async () => {
// 锁定当前行,防止其他事务修改
const record = await db.table.find(1).forUpdate();
// 锁定多个表
await db.table.forUpdate(['table1', 'table2']);
});
forNoKeyUpdate
添加 FOR NO KEY UPDATE 锁(不锁定索引键):
await db.table.find(1).forNoKeyUpdate();
forShare
添加 FOR SHARE 锁(共享锁,允许其他事务读):
await db.table.find(1).forShare();
forKeyShare
添加 FOR KEY SHARE 锁(索引键共享锁):
await db.table.find(1).forKeyShare();
skipLocked
跳过被锁定的行,无可用行时返回空:
await db.table.forUpdate().skipLocked(); // 跳过锁定行
noWait
锁定冲突时立即报错,不等待:
await db.table.forUpdate().noWait(); // 锁定失败时抛出异常
日志记录
通过 { log: true } 启用事务日志(包括 BEGIN/COMMIT):
await db.$transaction({ log: true }, async () => {
await db.table.insert({ name: 'log-test' });
// 控制台将输出完整 SQL 语句
});