Strapi 数据库事务详解:strapi.db.transaction 的原子操作、嵌套事务与 onCommit/onRollback 钩子源码解析
Strapi 数据库事务详解strapi.db.transaction 的原子操作、嵌套事务与 onCommit/onRollback 钩子源码解析【免费下载链接】strapi Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi本篇围绕 Strapi 官方文档中的 Transactions事务指南展开讲解如何通过strapi.db.transaction将一组数据库操作包装为具备原子性的单元覆盖 handler 参数、嵌套事务、onCommit/onRollback回调与 knex 原生查询的协作方式并结合当前仓库packages/core/database的源码剖析事务上下文基于AsyncLocalStorage的隐式传播机制、双重终结防护以及实体管理器内部对事务 API 的真实调用链帮助你在编写 Strapi 自定义 controller、service 或插件时正确使用事务并规避锁竞争与连接悬挂问题。实验性功能说明Strapi 官方文档 02-transactions.md 在文首明确标注This is an experimental feature and is subject to change in future versions.这是一个实验性功能可能在未来的版本中发生变更。因此本文描述的所有 API 形态与行为以当前仓库版本为准在生产项目中引入时应关注版本升级后的兼容性。什么是事务事务是一组作为单一单元共同执行的操作。若其中任何一个操作失败整个事务失败数据回滚到事务开始前的状态若所有操作都成功事务被提交数据被永久写入数据库。这一语义保证了“要么全部成功要么全部不生效”的原子性是处理多表/多记录关联写入的基础手段。基本用法strapi.db.transaction事务通过向strapi.db.transaction传入一个 handler 函数来处理。handler 内的所有strapi.db查询会隐式接入当前事务await strapi.db.transaction(async ({ trx, rollback, commit, onCommit, onRollback }) { // 以下操作自动使用该事务 await strapi.db.create(); await strapi.db.create(); });handler 执行完毕后若所有操作成功事务自动提交若任一操作抛出异常事务自动回滚数据恢复到先前状态。官方文档同时强调事务块内执行的每一个strapi.db.query操作都会隐式使用该事务。这一点在当前仓库源码中有明确实现——查询构建器的执行阶段会读取事务上下文并自动挂接.transacting()详见下文“隐式事务传播机制”一节。handler 参数说明handler 函数接收一个对象参数各属性含义如下完整继承自官方文档属性说明trx事务对象。可用于在事务内执行 knex 原生查询。commit提交事务的函数。rollback回滚事务的函数。onCommit注册一个回调在事务提交后执行。onRollback注册一个回调在事务回滚后执行。从源码结构看这五个属性在 Database#transaction 中被组装为callbackParams传入 handler其中commit/rollback是两个包装函数——只有最外层事务调用它们时才会真正触发底层 Knex transactor 的提交/回滚嵌套事务中调用则为空操作这正是“嵌套事务随外层提交或回滚”语义的实现基础。程序化用法不传 handler 获取事务对象文档正文只展示了 handler 形式但从 index.ts 的重载定义可以看到transaction()还支持不传回调直接返回事务对象的调用方式transaction(): PromiseTransactionObject; transactionTCallback extends Callback(c: TCallback): PromiseReturnTypeTCallback;不传回调时返回{ commit, rollback, get }三个成员const trx await strapi.db.transaction(); try { // 注意此模式下 strapi.db 查询不会自动挂接需手动 transacting await strapi.db.query(api::user.user) .create({ data: { email: foobar.com } }); await trx.commit(); } catch (err) { await trx.rollback(); }这种“对象式”用法在当前仓库内部被大量使用实体管理器entity manager在对关联关系做attachRelations、updateRelations、deleteRelations等操作时就是先const trx await db.transaction()再在 try/catch 中显式调用trx.commit()/trx.rollback()见 entity-manager/index.ts。这提示我们在需要手动控制提交时机如跨多次外部调用分段提交的场景下可以直接使用对象式 API。嵌套事务事务可以嵌套。嵌套时内层事务的提交或回滚会跟随外层事务的提交或回滚——即内层的“commit”只是提前结束内层作用域真正的数据库提交发生在外层。await strapi.db.transaction(async () { // 隐式使用外层事务 await strapi.db.create(); // 嵌套事务会隐式使用外层事务 await strapi.db.transaction(async ({}) { await strapi.db.create(); }); });从源码看Database#transaction 通过transactionCtx.get()判断当前是否已处于事务中若已存在事务上下文则复用现有的Knex.Transaction对象notNestedTransaction为 false不再开启新的数据库事务同时commit/rollback包装函数在notNestedTransaction为 false 时直接跳过底层调用。因此内层“提交”不会真正落库只有最外层 handler 正常返回时才会在 index.ts 中执行await commit()真正提交。onCommit 与 onRollback 钩子onCommit和onRollback用于在事务提交后或回滚后执行代码典型用途包括提交后发送通知邮件、刷新缓存、清理临时资源等“必须在最终状态确定后”才能执行的逻辑await strapi.db.transaction(async ({ onCommit, onRollback }) { // 隐式使用事务 await strapi.db.create(); await strapi.db.create(); onCommit(() { // 事务提交后执行 }); onRollback(() { // 事务回滚后执行 }); });源码中该机制的实现位于 transaction-context.ts。其关键设计有几点值得注意回调挂存在当前事务作用域的 store 上。onCommit/onRollback将回调 push 进AsyncLocalStorage当前 store 的commitCallbacks/rollbackCallbacks数组见 transaction-context.ts。提交后执行并清空。commit(trx)在成功调用trx.commit()后逐个执行store.commitCallbacks并清空数组transaction-context.ts保证回调不会重复触发。嵌套事务会继承外层回调列表。transactionCtx.run在开启新 store 时以store?.commitCallbacks || []初始化transaction-context.ts因此内层事务注册的onCommit回调最终也会随外层提交一起执行——这与嵌套事务“随外层终结”的语义一致。双重终结防护。提交/回滚前会检查 Knex transactor 的isCompleted()transaction-context.ts、L50-L57如果事务已经被终结例如 handler 中先手动调用了rollback又正常返回则跳过第二次commit/rollback调用避免抛出 “Transaction query already complete” 之类的错误。配合 knex 原生查询使用事务也可以与 knex 查询配合但必须显式调用.transacting(trx)因为原生 knex 查询不走 Strapi 的查询构建器无法自动读取事务上下文await strapi.db.transaction(async ({ trx, rollback, commit }) { await knex(users).where(id, 1).update({ name: foo }).transacting(trx); });这里的knex实例在 Strapi 项目中即strapi.db.connection可通过 Database#getConnection 获取它会自动处理 schema 前缀。隐式事务传播机制为什么 strapi.db 查询会自动进事务官方文档提到“事务块内每一个strapi.db.query操作都会隐式使用该事务”其实现链路如下上下文存储transaction-context.ts 使用 Node.js 的AsyncLocalStorage创建storage在transactionCtx.run(trx, cb)中将{ trx, commitCallbacks, rollbackCallbacks }作为 store 写入异步上下文。这样即使事务跨越多个await与异步函数边界strapi.db的任何后续调用都能通过transactionCtx.get()拿到同一个trx。查询执行时挂接查询构建器的execute阶段会检查上下文query-builder.tsasync execute({ mapResults true } {}) { const qb this.getKnexQuery(); const transaction transactionCtx.get(); if (transaction) { qb.transacting(transaction); } const rows await qb; // ... }一旦上下文存在事务所有经strapi.db.query(uid)发起的 select/insert/update/delete 都会带上.transacting(transaction)由 Knex 将其路由到同一连接与事务内执行。判断是否在事务中Database类暴露了inTransaction()方法index.ts内部即!!transactionCtx.get()可在自定义代码中用于分支判断。实体管理器内部对事务的使用事务 API 并非仅供用户调用Strapi 数据层自身就依赖它保证关联关系写入的原子性。以 entity-manager/index.ts 为例attach附加关联的内部流程是开启对象式事务 → 执行attachRelations内部所有 join 表插入均.transacting(trx.get())→ 成功则trx.commit()异常则trx.rollback()并向上抛出。updateRelations、deleteRelationsentity-repository.ts遵循完全相同的模式。此外batchInsertJoinTable的源码注释明确要求“必须传入活跃事务”其原因是批量插入 join 表需要“要么全部提交、要么全部回滚”的原子性entity-manager/index.ts。这意味着当你在业务层用strapi.db.transaction包裹一次含关联的update时内层的关联写入会嵌套进你的外层事务整体作为一次原子操作提交或回滚。何时使用事务官方文档给出的判断标准是多个操作应当一起执行、且彼此依赖时才应使用事务。文档示例创建新用户时既要写库又要发送欢迎邮件——若邮件发送失败用户也不应被创建因此两者需要绑定在同一原子语义下处理。反过来对于彼此不依赖的操作不应使用事务因为过大的事务范围会带来性能损耗文档 “When not to use transactions” 一节。事务的潜在问题文档 “Potential problems of transactions” 一节指出了两类风险使用事务时需保持谨慎锁竞争事务内执行多个操作可能产生行锁/表锁阻塞其他进程的事务直到本事务完成。事务范围应尽量小只包裹真正需要原子性的写入操作把非事务性耗时操作网络请求、文件生成等移出事务。事务悬挂如果事务被开启但未正确提交或回滚连接可能被无限期占用导致系统不稳定直到服务重启、连接被强制关闭为止。这类问题难以调试。对此当前源码已通过两层机制降低风险一是 handler 形式下由 index.ts 的 try/catch 保证“正常返回必提交、抛错必回滚”二是isCompleted()防护避免对已终结事务的二次操作抛错transaction-context.ts。测试用例验证仓库内的单元测试 index.test.ts 中的Transaction测试块对上述行为给出了可验证的断言分别针对 connection object 与 connection function 两种配置形态执行handler 正常返回时事务返回值被透传且底层commit被调用恰好 1 次handler 抛出异常时rollback被调用恰好 1 次且原始错误会向外 rethrow当 transactor 已通过isCompleted()标记为终结时不会再触发第二次commit或rollback双重终结防护。这些测试与上文源码分析相互印证可作为你自研代码中引用事务行为的依据。参考路径汇总内容路径官方事务文档02-transactions.mdDatabase类与transaction()实现packages/core/database/src/index.tsAsyncLocalStorage事务上下文与回调机制packages/core/database/src/transaction-context.ts查询执行时隐式挂接事务packages/core/database/src/query/query-builder.ts实体管理器内部事务调用链packages/core/database/src/entity-manager/index.ts事务单元测试packages/core/database/src/tests/index.test.ts适用前提以上结论基于当前仓库版本的strapi/database包实现事务功能在官方文档中标记为 experimental跨大版本升级时建议重新核对该 API 的行为与签名。【免费下载链接】strapi Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →