查数据必须用Db::query()返回二维数组,改数据必须用Db::execute()返回影响行数;二者语义严格分离,混用会导致逻辑错误或静默失败,且在读写分离下分别固定走读库和写库。

Db::query 和 Db::execute 到底该用哪个
查数据用 Db::query,改数据用 Db::execute——不是凭感觉,是底层行为决定的。前者走 PDO::fetchAll,返回数组;后者走 PDO::exec,只返回影响行数。
常见翻车点:
- 用
Db::query执行DELETE FROM log,返回空数组,误以为没删成,其实全删了 - 用
Db::execute执行SELECT * FROM user,返回false或整数,根本拿不到结果 - TP6 支持问号占位符:
Db::query("SELECT * FROM user WHERE id > ? AND status = ?", [10, 'active']);TP5 不支持,硬拼字符串必须自己转义,否则 SQL 注入风险拉满
事务里执行原生 SQL 为什么 rollback 失效
核心就两条:语句本身不参与事务 + 框架没感知到失败。
MySQL 中像 CREATE TEMPORARY TABLE、SET @var = 1、DROP TABLE 这类语句,PDO 执行后直接提交,rollback 完全无效——这不是 ThinkPHP 的 bug,是 MySQL 的设计。
立即学习“PHP免费学习笔记(深入)”;
更隐蔽的问题是:你在 Db::startTrans() 后调 Db::execute("UPDATE ..."),哪怕 SQL 执行出错但没抛异常(比如影响行数为 0),框架就当“成功”,继续走到 commit()。
所以务必做到:
- DDL / 变量赋值 / 临时表等语句,别塞进事务块
- 原生写操作后检查返回值:
if (false === Db::execute(...)) { Db::rollback(); throw new \Exception('SQL failed'); } - 所有原生操作必须在
Db::startTrans()之后、Db::commit()之前,且不能跨连接(比如手动Db::connect('slave')就脱离事务上下文)
Db::transaction() 闭包里异常被吞掉就等于没事务
Db::transaction() 能自动回滚,前提是异常得“穿出去”。一旦你在闭包里 try-catch 住,或者 return 提前退出,框架就认为执行完毕,自动 commit()。
错误写法示例:
Db::transaction(function () {
Db::table('user')->insert(['name' => 'a']);
try {
Db::table('log')->insert(['msg' => 'error']);
} catch (\Exception $e) {
// 异常被吃掉,事务已提交
}
});正确做法只有两种:
- 让异常穿透:
Db::transaction(function () { ... throw new \Exception('boom'); }); - 改用手动模式:
Db::startTrans()+try/catch+ 显式rollback()/commit()
注意:Db::transaction() 不支持嵌套。外层调一次,内层再调一次,第二次 startTrans 实际被忽略,rollback 只退到第一层。
回滚后连接可能已损坏,别接着用
事务回滚不会重置 PDO 连接状态。如果回滚是因为死锁、超时或语法错误触发的,连接可能卡在“unbuffered query active”之类的状态,后续任何查询都会直接报 PDOException: SQLSTATE[HY000]: General error。
验证方式很简单:在事务末尾加 throw new \Exception('test'),看数据是否消失——消失说明事务生效;没消失,要么根本没进事务,要么连接被切走了。
生产环境建议:
- 回滚后不要再复用当前连接做新查询,尤其不要在
catch块里紧接着调Db::table()->select() - 如需记录错误日志并继续执行,先
Db::connect()->close()断开,或换新连接实例 - 确认表引擎是
InnoDB,MyISAM 下事务形同虚设



















