PHP Conference Japan 2024

预处理语句

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 不包含客户端预处理语句模拟的模拟。

另请参阅

添加注释

用户贡献注释

此页面没有用户贡献的注释。
To Top