PAYATHON 2026

PHP 调用 MySQL Stored Procedure 时无法获取记录

数据阿明

结论

CALL tabla_posiciones(?,?,?,?) 可以通过 mysqli prepared statement 返回记录。$contador 一直是 0,通常不代表 Stored Procedure 不能被 PHP 调用,更可能是以下原因:

  1. prepare()、bind_param()、execute()、bind_result() 或 fetch() 执行失败,但代码没有检查错误。
  2. PHP 实际连接的 MySQL 实例、数据库或 MySQL 账户上下文与 Workbench 不同。
  3. Stored Procedure 返回了多个结果集,PHP 没有正确读取和清理。
  4. 如果 tabla_pos 是真正的 TEMPORARY TABLE,它只对创建它的数据库会话可见,Workbench 和 PHP 连接无法共享。
  5. PHP 中判断 $contador 的代码有误。题目里的 if ($contador "S/N"); 不是合法的 PHP,正常写法应为 if ($contador === 0)。
  6. 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 === []) {
    // 没有记录
}

推荐的排查顺序

  1. 启用 MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT,获取确切错误。
  2. 检查 PHP 实际收到的四个参数,并用 1, 1, 7, 5 作为固定值进行对照。
  3. 通过 PHP 连接查询 DATABASE()、@@hostname、USER() 和 CURRENT_USER()。
  4. 使用完整名称 CALL eschema.tabla_posiciones(?, ?, ?, ?)。
  5. 检查 execute()、bind_result() 和 fetch() 的返回状态。
  6. 确认实际结果集包含九列,并与九个绑定变量逐一对应。
  7. 读取主要结果后,清理 CALL 产生的剩余结果集。
  8. 确认 tabla_pos 是否为会话级临时表,以及它是否在 PHP 当前连接中创建并填充。
  9. 检查关闭自动提交后产生的事务和锁影响。
  10. 将记录数判断修正为 $contador === 0。

Workbench 能返回结果,只能说明 Stored Procedure 在 Workbench 的连接中,使用当时的会话状态和参数可以正常工作。要查清 PHP 为什么得到空结果,需要逐项确认数据库错误、连接身份、输入参数和实际结果集,不能把所有失败都当成“没有记录”。

备注:内容仅供参考。