PHP 调用 MySQL Stored Procedure 时无法获取记录
结论
CALL tabla_posiciones(?,?,?,?) 可以通过 mysqli prepared statement 返回记录。$contador 一直是 0,通常不代表 Stored Procedure 不能被 PHP 调用,更可能是以下原因:
prepare()、bind_param()、execute()、bind_result()或fetch()执行失败,但代码没有检查错误。- PHP 实际连接的 MySQL 实例、数据库或 MySQL 账户上下文与 Workbench 不同。
- Stored Procedure 返回了多个结果集,PHP 没有正确读取和清理。
- 如果
tabla_pos是真正的TEMPORARY TABLE,它只对创建它的数据库会话可见,Workbench 和 PHP 连接无法共享。 - PHP 中判断
$contador的代码有误。题目里的if ($contador "S/N");不是合法的 PHP,正常写法应为if ($contador === 0)。 - Stored Procedure 内部发生了 SQL 错误,或者最终的
SELECT在 PHP 会话中确实返回了空集。
应先启用严格错误报告,找到具体失败的步骤,再处理结果集。不能只根据 $contador 判断数据库中没有记录。
首先输出真实的 MySQL 错误
原代码只检查了 prepare(),没有检查 bind_param()、execute()、bind_result() 和 fetch()。例如,execute() 失败后代码仍然继续运行,最后也可能表现为循环一次都没有进入。
开发环境可以这样处理:
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$db = new mysqli('localhost', 'user', 'password', 'eschema');
$db->set_charset('utf8mb4');
$cliente = filter_input(INPUT_POST, 'id_cliente', FILTER_VALIDATE_INT);
$sucursal = filter_input(INPUT_POST, 'id_sucursal', FILTER_VALIDATE_INT);
$torneo = filter_input(INPUT_POST, 'id_torneo', FILTER_VALIDATE_INT);
$jornada = filter_input(INPUT_POST, 'id_jornada', FILTER_VALIDATE_INT);
if (
$cliente === false || $cliente === null ||
$sucursal === false || $sucursal === null ||
$torneo === false || $torneo === null ||
$jornada === false || $jornada === null
) {
throw new InvalidArgumentException('Stored Procedure 参数无效');
}
$stmt = $db->prepare(
'CALL eschema.tabla_posiciones(?, ?, ?, ?)'
);
$stmt->bind_param(
'iiii',
$cliente,
$sucursal,
$torneo,
$jornada
);
$stmt->execute();
启用 MYSQLI_REPORT_STRICT 后,MySQL 错误会以异常形式抛出,包括权限不足、表不存在、参数错误、结果列数量不符,以及 Stored Procedure 内部 SQL 执行失败等问题。
生产环境中,不要把数据库密码、SQL 内容或完整的异常堆栈直接返回给客户端。应将详细信息写入服务器日志,只向客户端返回通用错误信息。
读取 prepared statement 的结果
如果服务器安装了 mysqlnd,可以直接使用 get_result():
$stmt->execute();
$result = $stmt->get_result();
$rows = [];
while ($row = $result->fetch_assoc()) {
$rows[] = $row;
}
$result->free();
$stmt->close();
while ($db->more_results() && $db->next_result()) {
if ($extraResult = $db->store_result()) {
$extraResult->free();
}
}
if (count($rows) === 0) {
$response = [
'Datos' => [],
'status' => 1,
'mensaje' => 'No existen registros con los datos proporcionados',
];
} else {
$response = [
'Datos' => $rows,
'status' => 0,
'mensaje' => '',
];
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode(
$response,
JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
CALL 通常会在主要结果集之外产生过程结束状态。读取当前查询结果后,应使用 more_results() 和 next_result() 清理剩余结果。否则,同一连接执行下一条 SQL 时可能出现:
Commands out of sync; you can't run this command now
如果不能使用 get_result(),可以继续使用 bind_result():
$stmt->execute();
$stmt->store_result();
$stmt->bind_result(
$equ_nombre,
$jj,
$jg,
$je,
$jp,
$gf,
$ge,
$dif,
$puntos
);
$rows = [];
while (true) {
$fetchStatus = $stmt->fetch();
if ($fetchStatus === true) {
$rows[] = [
'equ_nombre' => $equ_nombre,
'jj' => $jj,
'jg' => $jg,
'je' => $je,
'jp' => $jp,
'gf' => $gf,
'ge' => $ge,
'dif' => $dif,
'puntos' => $puntos,
];
continue;
}
if ($fetchStatus === null) {
throw new RuntimeException(
'读取 Stored Procedure 结果失败:' . $stmt->error
);
}
break;
}
$stmt->free_result();
$stmt->close();
while ($db->more_results() && $db->next_result()) {
if ($extraResult = $db->store_result()) {
$extraResult->free();
}
}
mysqli_stmt::fetch() 有三种返回值:
true:成功读取一行;false:已经没有更多行;null:读取过程中发生错误。
原来的 while ($stmt->fetch()) 会把 false 和 null 都当作循环结束,因此可能掩盖实际发生的数据库错误。
检查返回列是否与 bind_result() 一致
Stored Procedure 最终的 SELECT 返回九列:
SELECT
equipo.equ_nombre,
tabla_pos.jj,
tabla_pos.jg,
tabla_pos.je,
tabla_pos.jp,
tabla_pos.gf,
tabla_pos.ge,
tabla_pos.gf - tabla_pos.ge AS dif,
tabla_pos.puntos
PHP 也绑定了九个变量,数量看起来一致。不过,还需要确认 PHP 实际调用的 Stored Procedure 就是当前检查的这个定义,而不是其他 schema 中的同名过程。
可以通过 PHP 连接执行:
SELECT DATABASE(), @@hostname, USER(), CURRENT_USER();
SHOW CREATE PROCEDURE eschema.tabla_posiciones;
这些信息可以确认:
- 当前默认数据库;
- PHP 连接的是哪台 MySQL 服务器;
- 客户端提交的登录身份;
- MySQL 实际用于权限检查的账户;
- PHP 调用的 Stored Procedure 是否与 Workbench 中检查的是同一个。
调用时建议写出完整名称:
$stmt = $db->prepare(
'CALL eschema.tabla_posiciones(?, ?, ?, ?)'
);
相同的用户名不一定对应同一个 MySQL 账户。MySQL 账户由用户名和来源主机共同确定,例如:
'user'@'localhost'
'user'@'%'
这两个账户可能具有不同的权限和配置。
确认 PHP 收到的参数确实是预期值
Workbench 使用固定参数:
CALL tabla_posiciones(1, 1, 7, 5);
PHP 使用的是 $_POST。字段存在并不代表内容正确,实际值可能是空字符串、带非数字字符的字符串,也可能是前端发送了 JSON,而不是表单数据。
调用前至少要验证并记录参数:
error_log(json_encode([
'cliente' => $cliente,
'sucursal' => $sucursal,
'torneo' => $torneo,
'jornada' => $jornada,
], JSON_UNESCAPED_UNICODE));
为了缩小排查范围,也可以暂时在 PHP 中使用固定值测试:
$cliente = 1;
$sucursal = 1;
$torneo = 7;
$jornada = 5;
如果固定值能返回记录,问题就在请求参数的发送或解析上,而不在 Stored Procedure 的调用方式。
特别检查 tabla_pos 是否为临时表
Stored Procedure 的注释将 tabla_pos 称为临时表,但代码片段没有显示它是如何创建的。
如果创建语句是:
CREATE TEMPORARY TABLE tabla_pos (...);
这张表只属于当前数据库连接。在 Workbench 中创建和填充的临时表,对 PHP 新建的 mysqli 连接不可见。连接池中的不同连接也无法共享它。
应让 Stored Procedure 在同一次调用和同一个会话中完成以下操作:
DROP TEMPORARY TABLE IF EXISTS tabla_pos;
CREATE TEMPORARY TABLE tabla_pos (...);
INSERT INTO tabla_pos (...);
-- 后续 cursor、UPDATE 和最终 SELECT
如果 tabla_pos 是普通持久表,还要考虑并发调用。多个请求同时删除、插入和更新同一张工作表时,数据可能互相覆盖。可以改用会话级临时表,或者为普通工作表增加调用标识,并让所有查询按该标识隔离。
检查事务设置
代码执行了:
$this->db->autocommit(FALSE);
Stored Procedure 中包含多条 UPDATE。关闭自动提交后,事务会一直持续,直到显式调用 commit()、rollback() 或关闭连接。
这通常不会妨碍当前连接读取自己刚更新的数据,但可能造成锁等待,也会使其他连接看不到这些修改。如果调用产生的修改需要保留,应明确提交事务:
try {
$stmt->execute();
// 读取所有结果
// ...
$db->commit();
} catch (Throwable $e) {
$db->rollback();
throw $e;
}
如果这个接口不需要手动控制事务,可以删除:
$this->db->autocommit(FALSE);
不要依赖析构时关闭连接来决定事务结果。
修正记录数判断
题目中的代码明显不完整:
if ($contador "S/N");
如果实际文件也是这样,PHP 会直接产生语法错误,不可能正常执行到返回 JSON 的位置。更可能的情况是,复制问题代码时漏掉了部分字符。
正确的空结果判断可以写成:
if ($contador === 0) {
$datos = [
['nombre' => 'S/N'],
];
$return_array = [
'Datos' => $datos,
'status' => 1,
'mensaje' => 'No existen registros con los datos proporcionados',
];
sendResponse(
200,
json_encode($return_array, JSON_UNESCAPED_UNICODE),
'application/json; charset=utf-8'
);
return true;
}
也可以不维护 $contador,直接检查结果数组:
if ($rows === []) {
// 没有记录
}
推荐的排查顺序
- 启用
MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT,获取确切错误。 - 检查 PHP 实际收到的四个参数,并用
1, 1, 7, 5作为固定值进行对照。 - 通过 PHP 连接查询
DATABASE()、@@hostname、USER()和CURRENT_USER()。 - 使用完整名称
CALL eschema.tabla_posiciones(?, ?, ?, ?)。 - 检查
execute()、bind_result()和fetch()的返回状态。 - 确认实际结果集包含九列,并与九个绑定变量逐一对应。
- 读取主要结果后,清理
CALL产生的剩余结果集。 - 确认
tabla_pos是否为会话级临时表,以及它是否在 PHP 当前连接中创建并填充。 - 检查关闭自动提交后产生的事务和锁影响。
- 将记录数判断修正为
$contador === 0。
Workbench 能返回结果,只能说明 Stored Procedure 在 Workbench 的连接中,使用当时的会话状态和参数可以正常工作。要查清 PHP 为什么得到空结果,需要逐项确认数据库错误、连接身份、输入参数和实际结果集,不能把所有失败都当成“没有记录”。
备注:内容仅供参考。
