PHP Conference Japan 2024

多语句

MySQL 可选择地允许在一个语句字符串中包含多个语句,但需要特殊处理。

多语句或多查询必须使用 mysqli::multi_query() 执行。语句字符串的各个语句用分号分隔。然后,必须获取执行的语句返回的所有结果集。

MySQL 服务器允许在一个多语句中包含返回结果集的语句和不返回结果集的语句。

示例 #1 多语句

<?php

mysqli_report
(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("example.com", "user", "password", "database");

$mysqli->query("DROP TABLE IF EXISTS test");
$mysqli->query("CREATE TABLE test(id INT)");

$sql = "SELECT COUNT(*) AS _num FROM test;
INSERT INTO test(id) VALUES (1);
SELECT COUNT(*) AS _num FROM test; "
;

$mysqli->multi_query($sql);

do {
if (
$result = $mysqli->store_result()) {
var_dump($result->fetch_all(MYSQLI_ASSOC));
$result->free();
}
} while (
$mysqli->next_result());

以上示例将输出

array(1) {
  [0]=>
  array(1) {
    ["_num"]=>
    string(1) "0"
  }
}
array(1) {
  [0]=>
  array(1) {
    ["_num"]=>
    string(1) "1"
  }
}

安全注意事项

API 函数 mysqli::query()mysqli::real_query() 不会设置在服务器中激活多查询所需的连接标志。使用多语句的额外 API 调用是为了减少意外 SQL 注入攻击的危害。攻击者可能会尝试添加诸如 ; DROP DATABASE mysql; SELECT SLEEP(999) 之类的语句。如果攻击者成功地将 SQL 添加到语句字符串中,但未使用 mysqli::multi_query(),则服务器将不会执行注入的恶意 SQL 语句。

示例 #2 SQL 注入

<?php
mysqli_report
(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("example.com", "user", "password", "database");
$result = $mysqli->query("SELECT 1; DROP TABLE mysql.user");
?>

以上示例将输出

PHP Fatal error:  Uncaught mysqli_sql_exception: You have an error in your SQL syntax; 
check the manual that corresponds to your MySQL server version for the right syntax to 
use near 'DROP TABLE mysql.user' at line 1

预处理语句

不支持在预处理语句中使用多语句。

参见

添加注释

用户贡献的注释 1 条注释

10
velthuijsen
6 年前
对示例 1 的建议改进。

原因
Multi_query 仅在返回数据/结果集时才返回非 false 响应,并且仅检查输入的第一个查询。将第一个 SELECT 查询与 INSERT 查询交换将导致示例过早退出,并显示消息“多查询失败:(0)”。
该示例假设一旦第一个查询没有失败,则其他查询也已成功。或者更确切地说,它只是在没有报告后续查询中的任何一个失败的情况下退出,因为如果查询失败,next_result 将返回 false。

示例中的更改是在创建字符串 $sql 之后进行的。



<?php
$mysqli
= new mysqli("example.com", "user", "password", "database");
if (
$mysqli->connect_errno) {
echo
"无法连接到 MySQL: (" . $mysqli->connect_errno . ") " . $mysqli->connect_error;
}

if (!
$mysqli->query("DROP TABLE IF EXISTS test") || !$mysqli->query("CREATE TABLE test(id INT)")) {
echo
"创建表失败: (" . $mysqli->errno . ") " . $mysqli->error;
}

$sql = "SELECT COUNT(*) AS _num FROM test; ";
$sql.= "INSERT INTO test(id) VALUES (1); ";
$sql.= "SELECT COUNT(*) AS _num FROM test; ";

// 示例 1 的更改从这里开始

// 不要费心检查 multi_query 的结果,因为它将在第一个查询没有返回数据时返回 false
// 即使查询本身成功。
$mysqli->multi_query($sql);

do
// while (true); // 仅在错误或没有更多查询需要处理时退出
{
// 检查当前正在处理的查询是否未失败
if (0 !== $mysqli->errno)
{
echo
"多查询失败: (" . $mysqli->errno . ") " . $mysqli->error;
break;
}

// 存储并可能处理查询的结果,
// store_result 和 use_result 都将返回 false
// 对于不返回结果的查询(例如 INSERT)
if(false !== ($res = $mysqli->store_result() )
{
var_dump($res->fetch_all(MYSQLI_ASSOC));
$res->free();
}

// 如果没有更多查询需要处理,则退出循环
if (false === ($mysqli->more_results() )
{
break;
}

// 获取要处理的下一个查询的结果
// 不要费心检查结果的成功/失败
// 因为在循环开始时有一个错误检查和
// 报告块。
$mysqli->next_result()

} while (
true); // 仅在错误或没有更多查询需要处理时退出
?>

请注意,正常的 while ($mysqli->more_results() && $mysqli->next_result() 已被两个检查和 while (true); 替换。
这是由于“问题” next_result 如果相关查询失败将返回 false。
因此,要么需要在 while 循环后进行最后一次检查以查看是否存在错误,要么必须将不同的操作拆分。
示例中的更改执行了拆分。
To Top