db::query() 无法处理多结果集是因为其底层 pdo::query() 默认只返回第一个结果集,后续结果需显式调用 nextrowset() 才能读取;它也不支持 out 参数绑定,必须用 pdo::prepare + bindvalue(..., ..., pdo::param_input_output) 并在 execute 后读取变量值。

Db::query() 能跑通简单存储过程,但遇到多结果集、OUT 参数、混合语句(SELECT + INSERT)时会直接失效——它只取第一个结果,后续全丢。必须切到 PDO 原生层操作。
为什么 Db::query() 无法处理多结果集
ThinkPHP 的 Db::query() 底层调用的是 PDO::query(),而该方法默认只返回第一个结果集;存储过程里有多个 SELECT、或穿插了 INSERT/UPDATE,后面的结果集根本不会被读取。
常见错误现象:$result = Db::query('CALL sp_multi_select(1)') 只拿到第一组数据,第二组 SELECT 完全消失,日志里也无报错。
- 每个
SELECT、每个INSERT、每个UPDATE都算一个独立结果集(哪怕没返回行) - 必须显式调用
$stmt->nextRowset()才能切换到下一个 -
nextRowset()返回false是正常结束信号,不是异常,别用try/catch包裹它 - 不调
nextRowset()就直接fetchAll(),永远只能看到第一个结果
如何正确绑定 OUT 参数并获取返回值
Db::query() 完全不支持 OUT 或 INOUT 参数绑定,传进去的变量不会被写回。必须用 PDO::prepare + bindValue + PDO::PARAM_INPUT_OUTPUT。
典型错误写法:$stmt->bindValue(':out_val', $out, PDO::PARAM_STR) —— 这只是当输入参数传进去了,过程执行完 $out 还是空的。
- 绑定前必须给变量赋初值,比如
$out = ''或$out = 0,否则 PDO 报SQLSTATE[HY000]: General error - 第三个参数必须是
PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT(字符串)或PDO::PARAM_INT | PDO::PARAM_INPUT_OUTPUT(整数) - 不能用
bindParam(),它要求变量已存在且可引用,而OUT值在execute()后才写入 -
execute()执行完后,再读$out的值,此时才是存储过程写入的结果
绕过框架封装:获取 PDO 实例并手动控制
别用 Db::getPdo()(TP6.1+ 已废弃),也别依赖 Db::query() 自动事务包装——存储过程内部可能已含事务逻辑,外层再套一层容易冲突。
实操路径:Db::getConnection()->getPdo() 拿到底层 PDO 对象,然后手动 prepare、bind、execute、nextRowset。
- 执行前加
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false),否则带参数的CALL可能解析失败 - 禁用 ThinkPHP 自动事务:
Db::startTrans()不要调,改用$pdo->beginTransaction()和$pdo->commit() - 每次执行完必须调
$stmt->closeCursor(),否则后续Db查询会报Commands out of sync - 不要设
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,它会让多结果集结构错乱,触发不可预测错误
TP8 / TP6 中最简可用模板
以下代码在 TP8 和 TP6.3+ 经实测可稳定处理带 OUT 参数 + 多结果集的存储过程:
$pdo = Db::connect()->getPdo();
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$stmt = $pdo->prepare('CALL sp_task_user_check_list(:task_id, :page, :limit, :keyword, @total)');
$stmt->bindValue(':task_id', $taskId, PDO::PARAM_INT);
$stmt->bindValue(':page', $page, PDO::PARAM_INT);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':keyword', $keyword, PDO::PARAM_STR);
$stmt->execute();
// 第一个结果集:主数据
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt->nextRowset(); // 切到下一结果集
// 第二个结果集:可能是 total 计数或其他 SELECT
$totalRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt->nextRowset(); // 继续推进(即使后面是空结果集)
// 读取 OUT 变量值
$totalCount = $pdo->query('SELECT @total AS total')->fetchColumn();
$stmt->closeCursor();
注意:如果存储过程本身不含 SELECT,只有 INSERT 和 SET @var = ...,那第一个 nextRowset() 就是为了跳过那个“空结果集”,才能让后续的 SELECT @total 正常执行。这个切换动作容易被忽略,但恰恰是成败关键。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











