Db::query()仅用于SELECT/SHOW/EXPLAIN等查询,返回二维数组;Db::execute()仅用于INSERT/UPDATE/DELETE等写操作,返回影响行数;二者混用将导致静默失败或逻辑错乱。

Db::query() 和 Db::execute() 必须严格按语义区分使用,混用会导致静默失败或逻辑错乱——这是 ThinkPHP 8.0 执行原生 SQL 最核心的约束。
Db::query() 只能查,不能改
它专用于返回结果集的语句:SELECT、SHOW、EXPLAIN、WITH 查询。成功时返回二维数组(空结果也是 []),失败返回 false。
- 错误写法:
Db::query("UPDATE user SET status = 1 WHERE id = ?", [123])—— 不报错,但返回空数组,数据根本没更新 - 正确写法:
Db::query("SELECT * FROM user WHERE status = ? AND name LIKE ?", [1, "%admin%"]) - 注意:LIKE 的通配符要拼在绑定值里,不是 SQL 字符串中,否则可能绕过预处理防注入
- TP8 默认用 PDO 真预处理,但 MySQL 8.0+ 下若未开启
PDO::ATTR_EMULATE_PREPARES => true,含问号的query可能直接抛SQLSTATE[HY000]: General error: 2034
Db::execute() 只能写,不能查
它专用于不返回结果集的语句:INSERT、UPDATE、DELETE、REPLACE、TRUNCATE、CREATE TEMPORARY TABLE(⚠️但该语句不支持事务回滚)。
- 错误写法:
Db::execute("SELECT id FROM user LIMIT 1")—— 返回整数(比如1),后续当数组遍历时直接崩溃 - 正确写法:
Db::execute("UPDATE user SET login_time = NOW() WHERE id = ?", [$uid]) - 想获取插入 ID?不能靠
execute返回值,得额外调Db::getLastInsID() - 批量插入优先走
Db::name('user')->insertAll($data),比手写多值 INSERT 更安全,也规避 SQL 长度限制
动态字段/表名必须白名单校验后拼接
Db::query() 和 Db::execute() 的占位符(? 或 :key)只绑定值,不解析字段名、表名、ORDER BY 方向等结构部分。
立即学习“PHP免费学习笔记(深入)”;
- 危险写法:
Db::query("SELECT * FROM :table WHERE id = ?", [$tableName, $id])——:table不会被替换,PDO 报语法错误 - 安全写法:先校验
$sortField是否在['id', 'name', 'create_time']中,再拼:"ORDER BY {$sortField} DESC" - 表前缀可用
__USER__,框架会自动替换,但仅限字面量,不能是变量:"SELECT * FROM __USER__ WHERE id = ?"✅;"SELECT * FROM {$prefix}user"❌(无校验、易注入)
事务中执行原生 SQL 要手动兜底
TP8 的 Db::startTrans() 不接管原生 SQL 的事务语义——尤其是非事务性语句,rollback 完全无效。
- 高危语句:CREATE TEMPORARY TABLE、SET @var=1、DROP TABLE、ALTER TABLE、CREATE INDEX —— 即使包在事务里,
Db::rollback()也无法撤销 - 必须做:每个原生操作后检查返回值,或用
try-catch捕获think\db\exception\DataNotFoundException和底层 PDO 异常 - 调试技巧:
Db::getLastSql()只记录最近一次执行的真实 SQL,别指望它反映整个事务块 - DDL 语句在 MySQL 8.0+ 上还可能因
PDO::ATTR_EMULATE_PREPARES关闭而被拒绝,务必确认数据库配置
真正容易被忽略的是:MySQL 8.0+ 环境下,哪怕只是执行一条带问号的 SELECT,如果 PDO 配置没显式打开 PDO::ATTR_EMULATE_PREPARES,就会直接崩在连接层,连错误日志都难定位。这个点不在文档显眼位置,但几乎每个升级到 TP8 + MySQL 8 的项目都会撞上。



















