Orchid ORM 事务

63 阅读3分钟

事务

事务中的所有查询均在同一数据库连接上执行,并将整个查询集作为一个原子单元运行。任何操作失败都会导致数据库回滚该连接上的所有查询,恢复至事务前的状态。

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 语句
});