本文详解 pg-promise 中事务(tx)与异步函数协同使用的常见陷阱,重点解决因 promise 传递不当导致的未捕获异常、事务不回滚等问题,并推荐现代写法替代已废弃的 `t.batch()`。
在使用 pg-promise 构建 Express 应用时,一个高频误区是:将已执行(即已返回 Promise 实例)的查询函数直接传入事务 t.batch(),而非在事务上下文内按需创建 Promise。这会导致两个严重问题:
首先,移除所有查询函数内部的 .catch()。错误应由事务层或顶层中间件统一捕获,确保原子性与可观测性:
// ✅ 清洁的查询函数:不处理错误,只返回 Promise
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 每个操作(推荐,更清晰可控),而非依赖 t.batch():
const transferEnvelopeBudgetByIds = async (req, res, next) => {
try {
const result = await db.tx(async t => {
// ✅ 所有操作均绑定到事务对象 `t`
const from = await addToColumn('envelopes', 'budget', req.envelopeFromId, -req.transferBudget, t);
const t
o = await addToColumn('envelopes', 'budget', req.envelopeToId, req.transferBudget, t);
return { from, to }; // 可选:返回结果供后续使用
});
req.updatedEnvelopes = result;
next();
} catch (err) {
// ✅ 所有数据库错误(连接失败、SQL 错误、约束冲突等)均在此被捕获
// 事务自动回滚,错误交由 Express 错误中间件处理
next(err);
}
};pg-promise 官方文档明确标注 Task.batch() 已废弃(obsolete)。其设计初衷是并行执行多个独立查询,但:
如需并行执行(且无依赖),可改用 Promise.all():
await db.tx(async t => {
return Promise.all([
addToColumn('envelopes', 'budget', req.envelopeFromId, -req.transferBudget, t),
addToColumn('envelopes', 'budget', req.envelopeToId, req.transferBudget, t),
]);
});? 提示:Promise.all() 中任一 Promise 拒绝,整个数组即拒绝,事务仍会回滚,符合预期。
db.on('error', error => {
console.error('pg-promise global error:', error);
});作为兜底,捕获极少数逃逸的底层连接异常。
遵循以上模式,即可彻底规避未捕获异常、事务不回滚等顽疾,写出健壮、可维护的数据库事务逻辑。