如何正确处理 pg-promise 中的批量事务与 Promise 错误捕获

本文详解 pg-promise 批量数据库操作中因 promise 传递不当导致的未捕获异常问题,指出 `t.batch()` 已废弃,并提供基于显式 `await` 和统一错误处理的现代事务写法。

在使用 pg-promise 构建事务性数据库操作时,一个常见陷阱是:将已执行(即已返回 Promise 实例)的查询函数直接传入 t.batch(),而非在事务上下文内按需构造 Promise。这会导致错误无法被事务层捕获,进而引发 Uncaught Exception——尤其在数据库连接失败、SQL 语法错误或约束冲突等场景下,Node.js 进程可能意外崩溃。

根本原因在于:

  • addToColumn(...) 若不接收事务对象 t,默认使用全局 db 实例执行,其返回的 Promise 脱离事务上下文
  • 当该 Promise 被提前调用(如 batchQuery([...]) 中直接传入 addToColumn(...) 调用结果),它会在 db.tx() 启动前就已开始执行,甚至可能在事务开启失败后仍尝试连接数据库;
  • 此时 .catch() 仅作用于 db.tx() 返回的 Promise,而内部“游离”的 Promise 抛出的错误无人监听,最终成为未捕获异常。

✅ 正确做法:所有数据库操作必须在事务回调函数内、通过事务对象 t 执行,并避免过早求值。推荐使用 async/await 显式控制流程,而非依赖已废弃的 t.batch():

// ✅ 推荐:参数化 + 可选事务上下文
const addToColumn = (tableName, columnName, entryId, amountToAdd, t = db) => {
  return t.one(
    'UPDATE ${table:name} SET ${column:name} = ${column:name} + ${amount:csv} WHERE id = ${id:csv} RETURNING *',
    {
      table: tableName,
      column: columnName,
      amount: amountToAdd,
      id: entryId,
    }
  );
};

// ✅ 推荐:事务内显式 await,自动回滚 + 统一错误传播
const transferEnvelopeBudgetByIds = async (req, res, next) => {
  try {
    const result = await db.tx(async t => {
      const from = await addToColumn(
        'envelopes',
        'budget',
        req.envelopeFromId,
        -req.transferBudget,
        t
      );
      const to = await addToColumn(
        'envelopes',
        'budget',
        req.envelopeToId,
        req.transferBudget,
        t
      );
      return { from, to }; // 可选:返回结构化结果
    });

    req.updatedEnvelopes = result;
    next();
  } catch (err) {
    // 所有错误(连接失败、SQL 错误、约束冲突)均由此处统一捕获
    // pg-promise 自动回滚事务,无需手动处理
    next(err);
  }
};

⚠️ 注意事项:

  • 不要使用 t.batch():官方文档明确标注其为 obsolete,且语义模糊(易误解为“并发执行”,实则顺序 resolve);现代写法应使用 await 链式调用,语义清晰、调试友好、错误可追溯。
  • 禁止提前执行查询:切勿在 db.tx() 外调用 addToColumn(...) 并将返回的 Promise 塞入数组——这等于绕过事务控制。
  • 错误处理集中化:事务内的 try/catch 或顶层 catch() 已足够;每个查询函数内部 .catch()(如原代码中的 handleQueryErr)反而会吞掉关键错误,破坏事务原子性。
  • 事务对象 t 是必需的:确保所有参与事务的查询都显式传入 t,否则它们运行在独立连接上,既不共享事务隔离级别,也无法联动回滚。

总结:pg-promise 的事务可靠性取决于 Promise 的构造时机执行上下文。坚持“事务内构造、事务内执行、顶层捕获”的三原则,即可彻底规避未捕获异常,并获得强一致的 ACID 保障。