必须显式开启事务,db::execute()和db::query()默认不参与自动事务管理;推荐用db::transaction()闭包包裹原生sql,可自动回滚,避免手动配对starttrans/commit/rollback出错。

ThinkPHP 执行原生 SQL 时必须显式开启事务,Db::execute() 和 Db::query() 默认不参与自动事务管理。 即使你用 Db::startTrans(),后续的 Db::execute() 也只在当前连接生效,不会自动绑定到事务上下文——这点和模型操作不同,容易误以为“开了事务就万事大吉”。
Db::startTrans() + Db::execute() 必须配对使用
原生 SQL 的事务控制完全依赖手动调用,框架不会帮你拦截或包装 Db::execute()。你得自己确保:所有关键 SQL 都在 startTrans() 之后、commit() 或 rollback() 之前执行。
-
Db::startTrans()只作用于当前数据库连接实例,不是全局开关 - 每个
Db::execute()都是独立语句,不会因前一句失败而自动中断后续执行 - 若中间混入模型操作(如
Db::table('user')->insert(...)),它会自动加入当前事务;但纯Db::execute('UPDATE ...')不会自动感知,除非你用的是同一个连接对象 - 推荐统一用
Db::execute()或统一用模型方法,避免混用导致事务漏控
用 Db::transaction() 包裹原生 SQL 最省心
闭包方式能自动处理开启、提交与回滚,哪怕里面全是 Db::execute(),只要抛出异常就会回滚——这是最不容易出错的方式。
Db::transaction(function () {
Db::execute('UPDATE user SET status = ? WHERE id = ?', [1, 1001]);
Db::execute('INSERT INTO log (msg) VALUES (?)', ['user activated']);
// 任意一句出错(比如字段不存在、主键冲突),整个事务自动 rollback
});
- 闭包内所有
Db::execute()共享同一事务上下文 - 不需要手写
try/catch,也不用记commit()放哪儿 - 如果必须在闭包外捕获错误(比如要记录日志),才需要自己加
try/catch并 re-throw
跨库执行 SQL 时事务不生效是常态
ThinkPHP 的事务只支持单库。如果你用 Db::connect('db2')->execute(...),它走的是另一个连接,Db::startTrans() 对它完全无效。
- 报错现象:
Db::commit()成功,但第二个库的 SQL 已经写入,无法回滚 - 根本原因:MySQL 本身不支持跨库事务(除非用 XA,TP 不内置支持)
- 可行解法:把多库操作拆成幂等步骤,用状态字段+定时任务补偿;或强制走同一物理库,用不同前缀区分逻辑表
- 别信“同时对两个
Db::connect()调用startTrans()就能事务一致”——这毫无意义,只是两个孤立事务
真正容易被忽略的是存储引擎:哪怕代码全对,ENGINE=MyISAM 的表执行 ROLLBACK 也完全没反应。上线前务必检查所有涉及表的 SHOW CREATE TABLE xxx,确认是 InnoDB。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











