PHP 的数据库查询扩展 PDO 的属性 ATTR_EMULATE_PREPARES 详解

947 阅读2分钟

在 PHP 中进行数据库操作时(通常为 MySQL),PDO 由于其安全性、封装行等特点被广泛使用。在使用 PDO 对 SQL 语句进行预编译时,有两种模式:既可以使用 MySQL 服务端引擎进行编译,也可以使用 PDO 扩展库进行仿真编译,具体使用哪种方式进行编译取决于 PDO 属性 ATTR_EMULATE_PREPARES 值

⒈ 仿真编译

含有占位符的 SQL 语句在 PDO 扩展中完成编译,并将占位符替换为相应的参数,上述操作完成后,SQL 语句会被发送给 MySQL 引擎执行,如果在执行过程中有错误发生,则会抛出异常(MySQL 默认使用仿真编译)。

① 使用仿真编译时,如果占位符使用命名参数的形式(即 :name 的形式),那么占位符可以在 SQL 语句中多次使用,但绑定值时只需要绑定一次

$dsn = 'mysql:dbname=test;host=127.0.0.1:charset=utf8mb4';
$user = 'test';
$password = 'test';

$pdo = new PDO($dsn, $username, $password);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

$sql = "SELECT * FROM test_table WHERE foo = :search OR bar = :search";

$stmt = $pdo->prepare($sql);
$stme->execute(['search' => $search]);➁②


② 在进行查询操作时,一次预编译完成后可以进行多次查询

$sql = "SELECT ?;  SELECT ?";
$stmt = $pdo->execute([1, 2]);

do {    
    $data = $stmt->fetchAll();    
    var_dump($data);
} while ($stmt->nextRowset());


⓷ 可以运行多种格式的 SQL 语句(MySQL 本地编译只能执行固定格式的 SQL 语句)

$sql = "SHOW TABLES LIKE ?";
$stmt = $pdo->prepare($sql);
$stmt->execute(["%$name%"]);

以上这种格式的 SQL 语句只能进行仿真编译,无法进行 MySQL 本地编译


④ 使用仿真编译时,如果绑定参数使用数组的方式通过 execute 方法进行传递,则所有的参数都将被当作字符串处理。这样,像 LIMIT ? OFFSET ? 进行参数绑定后将变为 LIMIT '10' OFFSET '10',在执行时会报错。

要解决上述问题,有两种方式:

ⅰ将 PDO::ATTR_EMULATE_PREPARES 设置为 FALSE,关闭仿真模式

ⅱ使用 bindParam 或 bindValue 进行参数绑定,绑定时明确数据类型

这里需要注意的是,PDO::PARAM_INT 并不会进行强制类型转换,所以在绑定参数时,参数的数据类型一定要正确,如下这种参数绑定 bindValue('1', PDO::PARAM_INT) 在执行时依然会报错

⒉ 本地编译

含有占位符的 SQL 语句会先发送给 MySQL 引擎进行预编译,然后再将绑定参数发送给 MySQL 引擎。如果 SQL 语句在编译时出错,则会抛出异常。

① 使用本地编译时,绑定参数必须与占位符一一对应(绑定参数的数量与占位符数量必须相等)

② 在执行数据库写操作时,可以编译一次,但使用不同的绑定参数执行多次

$data = [    
    1 => 1000,   
     5 =>  300,   
     9 =>  200,
];
$sql = "UPDATE test SET col1 = ? WHERE col2 = ?";$stmt = $pdo->prepare('$sql');

foreach ($data as $id => $bonus) {    
    $stmt->execute([$bonus, $id]);
}


⒊ 两种编译方式返回值格式的问题

当使用仿真编译时,查询得到的结果集中所有的数据格式都被转换成了 字符串,这是因为 PDOStatement 需要使用 getColumnMeta 方法解析查询字段的数据类型,但并不是所有的数据库引起都支持此方法,所以查询结果集中的数据都被转成了字符串。

使用本地编译则不会有上述的结果集中数据格式的问题,因为数据库引擎可以得到每个字段的数据类型。


⒋ 关于两种编译方式安全性的问题

无论是仿真编译还是本地编译,只要正确使用都不会有安全风险,但必须正确设置字符集,具体可以参考这里


⒌ bindParam 和 bindValue 的比较

在绑定参数时,bindValue 既可以i接受变量,也可以直接传值;但 bindParam 只能接受变量(因为 bindParam 的参数采用的是引用传递)。

但 bindParam 在进行绑定操作后,可以通过改变变量的值,多次执行 execute 得到不同的结果

$id = 10;
$sql = "SELECT * FROM test WHERE id = ?";
$stmt = $pdo->prepare($sql);
$stmt->bindParam($id, PDO::PARAM_INT);
$stmt->execute();
$res1 = $stmt->fatchAll();

// 这里改变 $id 的值,重新执行 execute 
$id = 30;
$stmt->execute();
$res2 = $stmt->fetchAll();

正式因为 bindParam 的参数采用引用传递的方式,才可以实现上例中的操作。