Db::query() 专用于 SELECT 等只读查询语句,返回二维数组;Db::execute() 专用于 INSERT/UPDATE 等写入操作,返回受影响行数。两者语义上严格区分,混用会导致数据无法查出、更新失败或静默无反馈。

Db::query() 和 Db::execute() 这两个方法,很多开发者容易混淆的关键点在于——它们并非“随意替换使用”。如果拿 query() 去执行修改数据的 SQL,或者用 execute() 去查询数据列表,结果往往是:数据查不出来、更新未生效、甚至悄无声息地失败。而且,在读写分离的架构下,还会将 SQL 发送到错误的数据库实例上。
查询数据必须用 Db::query(),切勿用它执行 UPDATE
query() 仅处理那些会产生结果集的语句:SELECT、SHOW、EXPLAIN、WITH。它返回的是一个二维数组,即使没有查询到数据,也会返回一个空数组 [],而不是 null。如果执行失败,则返回 false。
- 错误示例:
Db::query("UPDATE user SET status = 1 WHERE id = ?", [123])—— 这种写法不会报错,但检查数据库会发现数据纹丝不动。因为该 SQL 根本没有进入写库流程。 - 正确示范:
Db::query("SELECT * FROM user WHERE status = :status", ['status' => 1])。采用命名占位符绑定参数,可读性和可维护性都显著提升。 - TP8 也支持问号绑定,但
?的先后顺序必须与数组值严格对应,一旦顺序错乱,匹配结果就会出错。因此,推荐优先使用命名绑定(:param)。 - 调试时,
Db::getLastSql()仅记录最近一次调用。要获取正确的 SQL,必须确保该调用紧跟在query()或execute()之后。
修改数据必须用 Db::execute(),切勿用它查询列表
execute() 正好相反,它只处理那些没有结果集的写操作:INSERT、UPDATE、DELETE、REPLACE、TRUNCATE。它返回的是受影响的行数(整数),执行失败时同样返回 false。
- 踩坑示例:
$list = Db::execute("SELECT * FROM user"); foreach($list as ...)—— 这里$list实际上是一个数字,比如 0 或 1。用一个数字去 foreach,程序会直接报错。 - 正确做法:
$affected = Db::execute("UPDATE user SET name = ? WHERE id = ?", [$name, $id]); if ($affected === false) { /* 处理失败 */ }。先判断返回值是否为false,再决定后续逻辑。 - 如果需要获取最后一次插入的自增 ID,不能依赖
execute()的返回值,而应额外调用Db::getLastInsID()。 - 批量插入数据时,建议使用
Db::name('user')->insertAll($data)。它比手写原生 INSERT 语句更稳定,还能自动处理 SQL 长度超限问题。
动态字段/表名不能绑定,必须经白名单校验后拼接
有一个容易被忽略的细节::table 或 :order_field 这类占位符,在 PDO 预处理器中不会被真正替换。框架会将其视为普通字符串字面量,或者直接导致语法错误。
- 错误写法:
Db::query("SELECT * FROM :table WHERE id = ?", [$tableName, $id]) - 正确做法:先构建一个白名单数组,例如
['id', 'name', 'create_time'],然后校验$sortField是否在其中。校验通过后,再手动拼接:"ORDER BY {$sortField} DESC"。 - 表前缀可以使用
__USER__这种写法,框架会自动替换。但注意,这仅适用于静态写死的场景。如果表名本身是变量,仍需通过白名单校验后手动拼接。 - 编写 LIKE 模糊查询时,应将通配符包含在绑定值中:
['name' => "%{$keyword}%"]。不要将通配符直接写入 SQL 字符串。
MySQL 8.0+ 下原生 SQL 报错?大概率是 PDO 预处理未开启模拟
如果你使用的是 MySQL 8.0 及以上版本,执行原生 SQL 时偶尔会遇到 SQLSTATE[HY000]: General error: 2034 之类的错误。原因在于 TP8 默认未开启 PDO::ATTR_EMULATE_PREPARES => true,而 MySQL 8.0+ 对原生预处理的要求比早期版本更严格。
- 解决办法很简单:在
config/database.php文件中,对应的 MySQL 连接配置里,显式添加一行:'options' => [PDO::ATTR_EMULATE_PREPARES => true]。 - 有一个重要的注意事项:DDL 语句(例如 CREATE TEMPORARY TABLE、ALTER TABLE)本身不支持事务回滚。因此,禁止在
Db::startTrans()事务块中执行这类操作。 - 事务中的所有原生操作,都必须包裹在
try-catch中。需要同时捕获think\db\exception\DataNotFoundException和底层的 PDO 异常。 - 即使只执行一个
Db::query(),只要没有抛出异常,框架就会认为“一切正常”,不会自动中断事务。是否回滚,完全取决于你是否在catch块中主动throw或调用rollback()。
真正容易被忽视的是:事务边界和语句兼容性,从来不是靠框架自动兜底的。一句 SET @var = 1 或 DROP TABLE 写进去,即使外面套了事务,rollback() 也形同虚设。
