MySQL 数据库支持预处理语句。预处理语句或参数化语句用于以高效率重复执行相同的语句,并防止 SQL 注入。
基本工作流程
预处理语句执行包括两个阶段:准备和执行。在准备阶段,将语句模板发送到数据库服务器。服务器执行语法检查并初始化服务器内部资源以供将来使用。
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, label TEXT)");
/* 预处理语句,阶段 1:准备 */
$stmt = $mysqli->prepare("INSERT INTO test(id, label) VALUES (?, ?)");
/* 预处理语句,阶段 2:绑定和执行 */
$id = 1;
$label = 'PHP';
$stmt->bind_param("is", $id, $label); // "is" 表示 $id 绑定为整数,$label 绑定为字符串
$stmt->execute();
重复执行
预处理语句可以重复执行。每次执行时,都会评估绑定变量的当前值并将其发送到服务器。语句不会再次解析。语句模板不会再次传输到服务器。
示例 #2 预处理一次,执行多次的 INSERT
<?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, label TEXT)");
/* 预处理语句,阶段 1:准备 */
$stmt = $mysqli->prepare("INSERT INTO test(id, label) VALUES (?, ?)");
/* 预处理语句,阶段 2:绑定和执行 */
$stmt->bind_param("is", $id, $label); // "is" 表示 $id 绑定为整数,$label 绑定为字符串
$data = [
1 => 'PHP',
2 => 'Java',
3 => 'C++'
];
foreach ($data as $id => $label) {
$stmt->execute();
}
$result = $mysqli->query('SELECT id, label FROM test');
var_dump($result->fetch_all(MYSQLI_ASSOC));
上面的示例将输出
array(3) { [0]=> array(2) { ["id"]=> string(1) "1" ["label"]=> string(3) "PHP" } [1]=> array(2) { ["id"]=> string(1) "2" ["label"]=> string(4) "Java" } [2]=> array(2) { ["id"]=> string(1) "3" ["label"]=> string(3) "C++" } }
每个预处理语句都会占用服务器资源。语句应在使用后立即显式关闭。如果未显式关闭,则当 PHP 释放语句句柄时,该语句将被关闭。
使用预处理语句并不总是执行语句的最有效方法。仅执行一次的预处理语句会导致比非预处理语句更多的客户端-服务器往返。这就是上面 SELECT
未作为预处理语句运行的原因。
此外,请考虑使用 MySQL 多 INSERT SQL 语法进行 INSERT。对于该示例,多 INSERT 比上面显示的预处理语句需要更少的服务器和客户端之间的往返。
示例 #3 使用多 INSERT SQL 减少往返次数
<?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)");
$values = [1, 2, 3, 4];
$stmt = $mysqli->prepare("INSERT INTO test(id) VALUES (?), (?), (?), (?)");
$stmt->bind_param('iiii', ...$values);
$stmt->execute();
结果集值数据类型
MySQL 客户端-服务器协议为预处理语句和非预处理语句定义了不同的数据传输协议。预处理语句使用所谓的二进制协议。MySQL 服务器以二进制格式“按原样”发送结果集数据。结果在发送之前不会被序列化为字符串。客户端库接收二进制数据并尝试将值转换为适当的 PHP 数据类型。例如,来自 SQL INT
列的结果将作为 PHP 整数变量提供。
示例 #4 本机数据类型
<?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, label TEXT)");
$mysqli->query("INSERT INTO test(id, label) VALUES (1, 'PHP')");
$stmt = $mysqli->prepare("SELECT id, label FROM test WHERE id = 1");
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
printf("id = %s (%s)\n", $row['id'], gettype($row['id']));
printf("label = %s (%s)\n", $row['label'], gettype($row['label']));
上面的示例将输出
id = 1 (integer) label = PHP (string)
这种行为与非预处理语句不同。默认情况下,非预处理语句将所有结果返回为字符串。可以使用连接选项更改此默认设置。如果使用连接选项,则没有区别。
使用绑定变量获取结果
预处理语句的结果可以通过绑定输出变量或请求 mysqli_result 对象来检索。
输出变量必须在语句执行后绑定。必须为语句结果集的每一列绑定一个变量。
示例 #5 输出变量绑定
<?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, label TEXT)");
$mysqli->query("INSERT INTO test(id, label) VALUES (1, 'PHP')");
$stmt = $mysqli->prepare("SELECT id, label FROM test WHERE id = 1");
$stmt->execute();
$stmt->bind_result($out_id, $out_label);
while ($stmt->fetch()) {
printf("id = %s (%s), label = %s (%s)\n", $out_id, gettype($out_id), $out_label, gettype($out_label));
}
上面的示例将输出
id = 1 (integer), label = PHP (string)
预处理语句默认返回非缓冲结果集。语句的结果不会被隐式获取并从服务器传输到客户端以进行客户端缓冲。结果集会占用服务器资源,直到所有结果都被客户端获取为止。因此,建议及时使用结果。如果客户端未能获取所有结果,或客户端在获取所有数据之前关闭了语句,则必须由 mysqli
隐式获取数据。
也可以使用 mysqli_stmt::store_result() 缓冲预处理语句的结果。
使用 mysqli_result 接口获取结果
除了使用绑定结果外,还可以通过 mysqli_result 接口检索结果。 mysqli_stmt::get_result() 返回一个缓冲结果集。
示例 #6 使用 mysqli_result 获取结果
<?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, label TEXT)");
$mysqli->query("INSERT INTO test(id, label) VALUES (1, 'PHP')");
$stmt = $mysqli->prepare("SELECT id, label FROM test WHERE id = 1");
$stmt->execute();
$result = $stmt->get_result();
var_dump($result->fetch_all(MYSQLI_ASSOC));
上面的示例将输出
array(1) { [0]=> array(2) { ["id"]=> int(1) ["label"]=> string(3) "PHP" } }
使用 mysqli_result 接口提供了客户端结果集灵活导航的额外优势。
示例 #7 缓冲结果集以进行灵活读取
<?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, label TEXT)");
$mysqli->query("INSERT INTO test(id, label) VALUES (1, 'PHP'), (2, 'Java'), (3, 'C++')");
$stmt = $mysqli->prepare("SELECT id, label FROM test");
$stmt->execute();
$result = $stmt->get_result();
for ($row_no = $result->num_rows - 1; $row_no >= 0; $row_no--) {
$result->data_seek($row_no);
var_dump($result->fetch_assoc());
}
上面的示例将输出
array(2) { ["id"]=> int(3) ["label"]=> string(3) "C++" } array(2) { ["id"]=> int(2) ["label"]=> string(4) "Java" } array(2) { ["id"]=> int(1) ["label"]=> string(3) "PHP" }
转义和 SQL 注入
绑定变量与查询分开发送到服务器,因此不会干扰查询。服务器直接在执行点使用这些值,在解析语句模板后。绑定参数不需要转义,因为它们永远不会直接替换到查询字符串中。必须为服务器提供一个关于绑定变量类型的提示,以创建适当的转换。有关更多信息,请参见 mysqli_stmt::bind_param() 函数。
这种分离有时被认为是防止 SQL 注入的唯一安全功能,但如果所有值都被正确格式化,则可以使用非预处理语句实现相同程度的安全。应该注意的是,正确的格式化与转义不同,并且涉及比简单转义更多的逻辑。因此,预处理语句只是对此数据库安全元素的一种更方便、更不易出错的方法。
客户端预处理语句模拟
API 不包含对客户端预处理语句模拟的模拟。
另请参阅