thinkphp 6.x 安全执行原生sql需严格区分db::query()(只读,返回数组)与db::execute()(写操作,返回影响行数),参数值绑定、结构白名单校验、存储过程异常捕获缺一不可。

ThinkPHP 6.x 支持原生 SQL 和存储过程调用,但安全执行的关键不在于“能不能用”,而在于“怎么传参、怎么拼接、怎么收尾”。直接拼字符串等于把数据库钥匙交给用户;正确做法是值走绑定、结构走白名单、事务走闭环。
查数据用 Db::query(),改数据用 Db::execute()
两者语义和返回值完全不同,混用会导致逻辑错乱:
- SELECT、SHOW、EXPLAIN 等只读语句 → 必须用 Db::query(),返回二维数组,可直接 foreach 或赋值给模板
- INSERT、UPDATE、DELETE、REPLACE、TRUNCATE 等写操作 → 必须用 Db::execute(),返回影响行数(int),失败返回 false
- 常见错误:用 Db::execute() 执行 SELECT,结果是空或数字,后续遍历报错;用 Db::query() 执行 UPDATE,看似无报错,实则没生效(返回空数组,误判为“没执行”)
参数绑定只认值,字段名/表名/排序方向必须白名单校验
ThinkPHP 不支持 ? 或 :name 占位符绑定字段名、表名、ORDER BY 子句。这些属于 SQL 结构,PDO 预处理无法处理,硬塞会出错或被当字面量。
- ✅ 安全值绑定:Db::query("SELECT * FROM user WHERE status = ? AND name LIKE ?", [1, "%{$keyword}%"])
- ❌ 危险字段绑定:Db::query("SELECT * FROM ? WHERE id = ?", ['user', 123]) —— 报错或表名被当字符串
- 动态排序要先校验:in_array($sort, ['id', 'create_time', 'status']) ?: $sort = 'id'; $sql = "ORDER BY {$sort} DESC"
- 动态表名同理:只允许从预设列表中选取,禁止任何用户输入直入 FROM 或 JOIN
调用存储过程需注意返回类型与异常捕获
MySQL 存储过程常通过 SELECT 返回结果集,或用 OUT 参数传值。ThinkPHP 的 Db::query() 可接收多结果集,但需手动遍历。
- 调用示例:Db::query("CALL mypro_check_balance(:uid, :amount)", ['uid' => 1001, 'amount' => 500.00])
- 若过程含多个 SELECT,$resultSet 是二维数组,每个元素对应一个结果集;需按索引取值,如 $resultSet[0] 是第一个结果
- 务必用 try-catch 包裹,捕获 PDOException 或 think\db\exception\DataNotFoundException,避免未处理异常暴露数据库结构
- 存储过程中涉及事务控制(如 START TRANSACTION),需确认是否与 ThinkPHP 外层事务兼容;建议存储过程内部不开启新事务,由 PHP 层统一管理
IN 查询、空数组、长 SQL 的细节处理
这些场景容易因疏忽引发语法错误或注入漏洞,需针对性防护:
- IN 多值不能直接传数组:Db::query("WHERE id IN (?)", [[1,2,3]]) ❌ → 生成 IN ('1,2,3'),非法
- 正确方式:$ids = [1,2,3]; $in = str_repeat('?,', count($ids)-1) . '?'; Db::query("WHERE id IN ($in)", $ids)
- 空数组必须提前判断:if (empty($ids)) { return []; },否则 IN () 触发 SQL 语法错误
- 超长 SQL(如大批量 INSERT)建议改用 Db::name('table')->insertAll($data),更稳定且自动分段,避免 PDO 超限截断
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











