为什么PDO允许使用带命名占位符的索引数组,但只有在禁用仿真时才允许?



最近我了解到PDO允许使用索引数组与命名占位符,像这样:

$stmt->prepare("INSERT INTO TABLE (one, two) VALUES (:one, :two)");
$stmt->execute([1,2]);

令人惊讶的是,和我们更熟悉的

一样有效
$stmt->execute(["one" => 1, "two" => 2]);

我希望这段代码抛出一个错误,它确实抛出了一个错误,但只有当PDO模拟模式被关闭时-即当使用本机准备语句时:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$stmt = $pdo->prepare("select :one one, :two two");
$stmt->execute([1,2]);
var_dump($stmt->fetch(PDO::FETCH_ASSOC));
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
$stmt = $pdo->prepare("select :one one, :two two");
$stmt->execute([1,2]);
var_dump($stmt->fetch(PDO::FETCH_ASSOC));

,它为第一个代码片段输出一个常规数组,为第二个代码片段输出一个错误:

致命错误:SQLSTATE[HY093]: Invalid parameter number: parameter was not defined in at line: 9

这种行为似乎对大多数数据库驱动程序都很常见,除了Oracle(在两个代码片段上都出错)和Sqlite3(允许两个代码片段)。

我并不认为这种方法是个好主意,但我想了解它是如何实现的。

在我开始解释之前,让我们先说明一些事实:

  • MySQL不支持命名占位符
  • PDO支持命名和位置占位符,无论数据库驱动程序和仿真模式如何
  • 非仿真模式下,prepare()向MySQL发送SQL字符串,准备本地PS;在模拟模式下,SQL仅在execute()上发送,占位符替换值

解释当你用PDO_MySQL驱动程序在非仿真模式下调用prepare()时,PDO必须解析SQL并将其发送给MySQL。因为MySQL只支持位置占位符,PDO用?替换所有命名占位符。例如,如果您准备一个查询SELECT :name AS foo, PDO将强制发送SELECT ? AS foo

调用execute()时,PDO需要将参数绑定到占位符。此时的问题是SQL查询不再具有命名占位符。但是PDO会记住命名占位符,因为它们出现在映射到序数的SQL中。因为MySQL中的参数绑定是使用位置参数进行的,所以命名参数需要被重新映射到它们的序数,但位置参数不需要。它们只需要根据占位符映射检查,以确保数组索引对应于一个有效的占位符*

您可以在这里看到代码https://github.com/php/php-src/blob/master/ext/pdo/pdo_stmt.c#L44

Emulated的准备工作略有不同。没有参数绑定,所以整个逻辑不适用。当执行模拟PS时,解析器只是用从param数组中获取的带引号的字符串字面值(参见PDO::quote())替换每个命名占位符。这就是为什么命名占位符可以在SQL中出现多次,因为位置是不相关的。

值得指出的是,一些数据库驱动程序支持命名参数和/或在SQL中包含参数有不同的语法。


这个逻辑也解释了为什么命名占位符不能在非仿真准备中重用。

占位符列表在内部存储为字符串列表。即使您尝试在原生准备中两次使用命名占位符,代码也无法处理绑定。它将遍历列表并在第一个匹配处停止。例如,使用这样的SQL:

SELECT :one, :two, :one

地图如下:[':one', ':two', ':one']

如果你尝试执行它,将只有2个参数execute([':one' => 1, 'two' => 2])。它们将分别映射到位置1和2。这意味着位置3永远不会被映射到任何值。这就解释了为什么不能在原生PS中使用命名占位符。


*PDO实际上也将位置参数重新映射回命名参数,因为不同的驱动程序以不同的方式绑定参数,并且它需要健壮。参见Oracle命名绑定和位置绑定

最新更新