PHP嵌套循环调用存储过程时,第二个查询返回false问题求助
我有如下PHP代码:
$user_id = $user["id_user_key"]; $stmt = $db->prepare("CALL spGetUserProducts(?)"); $stmt->bind_param('i', $user_id); $stmt->execute(); $result = $stmt->get_result(); $data = array(); while($row = $result->fetch_assoc()) { $row_array = array(); $row_array["id"] = $row["id"]; $row_array["pname"] = $row["pname"]; $row_array["picon"] = $row["picon"]; $row_array["menuItems"] = array(); $product = $row["id"]; //loop $result_opt = $db->query("CALL spGetUserProductViews($user_id, $product)"); while ($opt_fet = $result_opt->fetch_assoc()) { $row_array["menuItems"][] = array( "id" => $opt_fet["id"], "vname" => $opt_fet["vname"], "isheader" => $opt_fet["isheader"] ); } array_push($data, $row_array); } $stmt->close(); echo json_encode($data);
第一个循环可正常获取$db连接,第一条预处理语句执行后能得到结果,但第二个查询语句返回false。不过将该语句放在循环外执行时却能正常工作,请问这是什么原因?
嗨,这个问题我之前排查过类似的情况,核心原因是MySQL对存储过程调用的结果集处理规则,再加上你代码里还藏着个SQL注入的坑,咱们一步步拆解:
1. 未关闭的结果集导致连接阻塞
当你调用第一个存储过程spGetUserProducts拿到$result后,在while循环遍历每一行的过程中,这个结果集其实并没有被完全释放。MySQL有个严格的规则:同一个数据库连接上,如果存在未处理完毕的结果集(包括存储过程返回的所有结果),你根本没法执行新的查询,哪怕是另一个存储过程调用也不行。
你把第二个查询放到循环外时,第一个结果集已经被完全遍历完,连接自动释放了资源,所以新查询能正常跑起来。
2. 额外的隐患:SQL注入
你现在第二个查询是直接把$user_id和$product拼进SQL字符串的:
$result_opt = $db->query("CALL spGetUserProductViews($user_id, $product)");
这是典型的SQL注入风险!哪怕这俩变量看起来是数字类型,也不能直接拼接,必须用预处理语句,不然哪天被恶意构造的数据钻空子就麻烦了。
修复步骤
第一步:先清掉第一个存储过程的所有结果集
在循环里执行第二个查询前,必须确保第一个语句的所有结果集都被处理完。存储过程有时候会返回多个结果集(哪怕你只用到一个),所以得调用$stmt->next_result()把多余的结果都清掉,释放连接资源。
第二步:用预处理语句调用第二个存储过程
把直接拼接SQL的写法改成预处理绑定参数的方式,既安全又符合MySQL的连接规范。
修改后的完整代码如下:
$user_id = $user["id_user_key"]; $stmt = $db->prepare("CALL spGetUserProducts(?)"); $stmt->bind_param('i', $user_id); $stmt->execute(); $result = $stmt->get_result(); $data = array(); while($row = $result->fetch_assoc()) { $row_array = array(); $row_array["id"] = $row["id"]; $row_array["pname"] = $row["pname"]; $row_array["picon"] = $row["picon"]; $row_array["menuItems"] = array(); $product = $row["id"]; // 关键操作:清理第一个存储过程的所有剩余结果集 while ($stmt->next_result()) { if ($res = $stmt->get_result()) { $res->free(); // 释放结果集资源 } } // 改用预处理调用第二个存储过程,避免注入 $stmt_opt = $db->prepare("CALL spGetUserProductViews(?, ?)"); $stmt_opt->bind_param('ii', $user_id, $product); // 两个int类型参数 $stmt_opt->execute(); $result_opt = $stmt_opt->get_result(); while ($opt_fet = $result_opt->fetch_assoc()) { $row_array["menuItems"][] = array( "id" => $opt_fet["id"], "vname" => $opt_fet["vname"], "isheader" => $opt_fet["isheader"] ); } // 及时关闭第二个语句的资源 $result_opt->free(); $stmt_opt->close(); array_push($data, $row_array); } // 清理第一个语句的资源 $result->free(); $stmt->close(); echo json_encode($data);
为什么这样能解决问题?
$stmt->next_result()会遍历第一个存储过程返回的所有结果集,确保连接上没有未处理的残留,这样新的查询就能顺利执行。- 预处理语句彻底杜绝了SQL注入风险,同时也遵循了MySQL的连接使用规范。
另外,还有个性能优化的小建议:可以考虑把两个存储过程的逻辑合并成一个,一次性查出所有产品和对应的视图数据,这样就不用在循环里反复请求数据库了,性能会提升不少哦。
内容的提问来源于stack exchange,提问作者dexter

