PHP Conference Japan 2024

PDO::prepare

(PHP 5 >= 5.1.0, PHP 7, PHP 8, PHP 8,PECL pdo >= 0.1.0)

PDO::prepare预处理一条语句用于执行,并返回语句对象

说明

public PDO::prepare(string $query, array $options = []): PDOStatement|false

预处理一条 SQL 语句,用于 PDOStatement::execute() 方法执行。语句模板可以包含零个或多个命名 (:name) 或问号 (?) 参数标记,当语句执行时,这些标记将被替换为实际值。不能在同一个语句模板中同时使用命名和问号参数标记;只能使用其中一种参数样式。使用这些参数来绑定任何用户输入,不要直接在查询中包含用户输入。

当调用 PDOStatement::execute() 时,您必须为要传入语句的每个值包含一个唯一的参数标记。在预处理语句中,不能多次使用同名的命名参数标记,除非启用了模拟模式。

注意:

参数标记只能表示完整的数据字面量。不能使用参数绑定字面量的一部分、关键字、标识符或任何任意查询部分。例如,不能在 SQL 语句的 IN() 子句中将多个值绑定到单个参数。

对将使用不同参数值多次发出的语句调用 PDO::prepare()PDOStatement::execute(),可以通过允许驱动程序协商客户端和/或服务器端缓存查询计划和元信息来优化应用程序的性能。此外,调用 PDO::prepare()PDOStatement::execute() 有助于防止 SQL 注入攻击,因为无需手动引用和转义参数。

对于本身不支持预处理语句/绑定参数的驱动程序,PDO 将模拟它们,并且如果驱动程序支持一种样式但不支持另一种样式,还可以将命名或问号样式参数标记重写为更合适的内容。

注意用于模拟预处理语句和重写命名或问号样式参数的解析器支持非标准的单引号和双引号反斜杠转义。这意味着紧接在反斜杠之前的终止引号不会被识别为此类引号,这可能会导致参数检测错误,从而导致预处理语句在执行时失败。解决方法是不对这些 SQL 查询使用模拟预处理,并通过使用驱动程序本身支持的参数样式来避免重写参数。

从 PHP 7.4.0 开始,可以通过加倍问号来转义它们。这意味着在将查询发送到数据库时,?? 字符串将被转换为 ?

参数

query

这必须是目标数据库服务器的有效 SQL 语句模板。

options

此数组包含一个或多个 key=>value 对,用于为此方法返回的 PDOStatement 对象设置属性值。最常见的是使用它将 PDO::ATTR_CURSOR 值设置为 PDO::CURSOR_SCROLL 以请求可滚动游标。某些驱动程序具有可以在预处理时设置的特定于驱动程序的选项。

返回值

如果数据库服务器成功预处理语句,PDO::prepare() 将返回一个 PDOStatement 对象。如果数据库服务器无法成功预处理语句,PDO::prepare() 将返回 false 或发出 PDOException(取决于错误处理)。

注意:

模拟的预处理语句不与数据库服务器通信,因此 PDO::prepare() 不检查语句。

错误/异常

如果属性 PDO::ATTR_ERRMODE 设置为 PDO::ERRMODE_WARNING,则发出级别为 E_WARNING 的错误。

如果属性 PDO::ATTR_ERRMODE 设置为 PDO::ERRMODE_EXCEPTION,则抛出 PDOException

示例

示例 #1 带有命名参数的 SQL 语句模板

<?php
/* 通过传递值数组来执行预处理语句 */
$sql = 'SELECT name, colour, calories
FROM fruit
WHERE calories < :calories AND colour = :colour'
;
$sth = $dbh->prepare($sql, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]);
$sth->execute(['calories' => 150, 'colour' => 'red']);
$red = $sth->fetchAll();
/* 数组键也可以加上冒号“:”前缀(可选)*/
$sth->execute([':calories' => 175, ':colour' => 'yellow']);
$yellow = $sth->fetchAll();
?>

示例 #2 带有问号参数的 SQL 语句模板

<?php
/* 通过传递值数组来执行预处理语句 */
$sth = $dbh->prepare('SELECT name, colour, calories
FROM fruit
WHERE calories < ? AND colour = ?'
);
$sth->execute([150, 'red']);
$red = $sth->fetchAll();
$sth->execute([175, 'yellow']);
$yellow = $sth->fetchAll();
?>

示例 #3 转义问号的 SQL 语句模板

<?php
/* 注意:这仅在 PostgreSQL 数据库中有效 */
$sth = $dbh->prepare('SELECT * FROM issues WHERE tag::jsonb ?? ?');
$sth->execute(['feature']);
$featureIssues = $sth->fetchAll();
$sth->execute(['performance']);
$performanceIssues = $sth->fetchAll();
?>

参见

添加注释

用户贡献的注释 17 条

182
匿名
11 年前
对于那些想知道为什么在占位符周围添加引号是错误的,以及为什么不能对表名或列名使用占位符的人

关于预处理语句中占位符的工作方式存在一个常见的误解:它们不仅仅是被替换为(转义的)字符串,然后执行生成的 SQL。相反,被要求“准备”语句的 DBMS 会为如何执行该查询制定一个完整的查询计划,包括它将使用哪些表和索引,无论您如何填写占位符,该计划都将相同。

对于 ":value" 的任何替换,"SELECT name FROM my_table WHERE id = :value" 的计划都将相同,但看似相似的 "SELECT name FROM :table WHERE id = :value" 无法计划,因为 DBMS 不知道您实际上要从哪个表中进行选择。

即使使用“模拟预处理”,PDO 也不能让您在任何地方使用占位符,因为它必须弄清楚您的意思: "Select :foo From some_table" 是否意味着 ":foo" 将成为列引用,还是文字字符串?

当您的查询使用动态列引用时,您应该明确地将您知道存在于表上的列列入白名单,例如,使用在 default: 子句中抛出异常的 switch 语句。
94
Simon Le Pine
11 年前
大家好,

第一次在 php.net 上发帖,有点紧张。

经过一番搜索,我了解了关于预处理语句的两件事
1.)如果用单引号 (') 括起来,它会失败
这会失败: "SELECT * FROM users WHERE email=':email'"
这会成功: "SELECT * FROM users WHERE email=:email"
2.)您不能使用预处理语句进行搜索
这会失败: "SELECT * FROM users WHERE :search=:email"
这会成功: "SELECT * FROM users WHERE $search=:email"

在我的情况下,我允许用户输入他们的用户名或电子邮件,确定他们输入了哪个,并将 $search 设置为“username”或“email”。由于此值不是由用户输入的,因此不存在 SQL 注入的可能性,因此可以像我一样安全地使用。

希望这可以为其他人节省大量搜索时间。
43
bg at enativ dot com
10 年前
如果您在循环中运行查询,请不要在循环内包含 $pdo->prepare(),这将为您节省一些资源(和时间)。

循环内的预处理语句
for($i=0; $i<1000; $i++) {
$rs = $pdo->prepare("SELECT `id` FROM `admins` WHERE `groupID` = :groupID AND `id` <> :id");
$rs->execute([':groupID' => $group, ':id' => $id]);
}

// 耗时 0.066626071929932 微秒

循环外的预处理语句
$rs = $pdo->prepare("SELECT `id` FROM `admins` WHERE `groupID` = :groupID AND `id` <> :id");
for($i=0; $i<1000; $i++) {
$rs->execute([':groupID' => $group, ':id' => $id]);
}

// 耗时 0.064448118209839 微秒

对于 1,000 个(简单)查询,它少用了 0.002 微秒。
不多,但值得一提。
58
daniel dot egeberg at gmail dot com
15 年前
您还可以将值数组传递给 PDOStatement::execute()。这也防止了 SQL 注入。您不一定必须使用 bindParam() 或 bindValue()。
3
theking2 at king dot ma
1 年前
占位符字符串存在限制。在以下代码中,第一个 execute 失败,并显示 SQLSTATE[HY093]。目前尚不清楚确切允许哪些字符。

<?php declare(strict_types=1);

$db = new \PDO("mysql:hostname=localhost;dbname=minidwh", "minidwh", "Meisterstueck!");
$db->query("SET NAMES 'utf8mb4'");

$db->query("DROP TABLE IF EXISTS `äëïöüß`");
$db->query("CREATE TABLE `äëïöüß` ( `id` INT NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`) ) ENGINE = ARIA;");
$db->query("ALTER TABLE `äëïöüß` ADD COLUMN `äëïöüß` TEXT NULL");
try {
$stmt = $db->prepare("INSERT INTO `äëïöüß` (`äëïöüß`) VALUES (:äëïöüß)");
$result = $stmt->execute([':äëïöüß' => 'test1']);
} catch (
\PDOException $e) {
echo
$e->getMessage() . '<BR>';
}

try {
$stmt = $db->prepare("INSERT INTO `äëïöüß` (`äëïöüß`) VALUES (?)");
$result = $stmt->execute(['test2']);
} catch (
\PDOException $e) {
echo
$e->getMessage() . '<BR>';
}

try {
$stmt = $db->prepare("INSERT INTO `äëïöüß` (`äëïöüß`) VALUES (:column)");
$result = $stmt->execute([':column' => 'test3']);
} catch (
\PDOException $e) {
echo
$e->getMessage() . '<BR>';
}

try {
$stmt = $db->prepare("INSERT INTO `äëïöüß` (`äëïöüß`) VALUES (:column)");
$stmt->bindValue(':column', 'test4');
$result = $stmt->execute();
} catch (
\PDOException $e) {
echo
$e->getMessage() . '<BR>';
}
42
admin at wdfa dot co dot uk
15 年前
关于预处理语句的 SQL 注入属性的说明。

仅当您使用 bindParam 或 bindValue 选项时,预处理语句才能防止 SQL 注入。

例如,如果您有一个名为 users 的表,其中包含两个字段:username 和 email,并且有人更新了他们的用户名,您可能会运行

UPDATE `users` SET `user`='$var'

其中 $var 将是用户提交的文本。

现在如果你做了
<?php
$a
=new PDO("mysql:host=localhost;dbname=database;","root","");
$b=$a->prepare("UPDATE `users` SET user='$var'");
$b->execute();
?>

如果用户输入了 User', email='test,那么将会发生注入,email 字段会被更新成 test,同时 user 字段会被更新成 User。

使用 bindParam,如下所示
<?php
$var
="User', email='test";
$a=new PDO("mysql:host=localhost;dbname=database;","root","");
$b=$a->prepare("UPDATE `users` SET user=:var");
$b->bindParam(":var",$var);
$b->execute();
?>

SQL 语句会被转义,用户名会被更新成 User', email='test'
5
Mark Simon
7 年前
许多学生倾向于在 SQL 语句中的字符串占位符周围添加单引号,因为他们在 SQL 和 PHP 中通常会在字符串周围添加单引号。

我必须解释一下

引号不是字符串的一部分 —— 它们用于在编程语言中构造字符串。如果你在 SQL 或 PHP 中创建一个字符串字面量,那么它确实必须用引号引起来。如果字符串已经创建,并且正在被传递,那么额外的引号往好了说是不正确的,往坏了说会被误解。

在预处理语句的占位符中,可以将占位符视为变量,无论它们是字符串还是其他值,它们总是不用引号编写。
15
public at grik dot net
12 年前
使用 PDO_MYSQL 时,你需要记住 PDO::ATTR_EMULATE_PREPARES 选项。

默认值为 TRUE,就像
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES,true);

这意味着调用 $dbh->prepare() 时不会创建预处理语句。调用 exec() 时,PDO 会自己将占位符替换为值,并向 MySQL 发送一个通用的查询字符串。

第一个后果是调用 $dbh->prepare('garbage');
不会报告错误。你将在调用 $dbh->exec() 时收到一个 SQL 错误。
第二个后果是在特殊情况下存在 SQL 注入风险,例如将占位符用于表名。

模拟的原因是 MySQL 在使用预处理语句时性能较差。模拟执行速度明显更快。
3
Robin
14 年前
使用预处理语句来确保二进制数据在存储和检索过程中的完整性。使用 sqlite_escape_string() 或 PDO::quote() 进行转义/引用不适用于二进制数据 —— 仅适用于文本字符串。

一个简单的测试验证了使用预处理语句可以完美地存储和检索数据

<?php

$num_values
= 10000;

$db = new pdo( 'sqlite::memory:' );

$db->exec( 'CREATE TABLE data (binary BLOB(512));' );

// 生成大量有问题的二进制数据
for( $i = 0; $i < $num_values; $i++ )
{
for(
$val = null, $c = 0; $c < 512/16; $c++ )
$val .= md5( mt_rand(), true );
@
$binary[] = $val;
}

// 通过预处理语句插入每个值
for( $i = 0; $i < $num_values; $i++ )
$db->prepare( 'INSERT INTO data VALUES (?);' )->execute( array($binary[$i]) );

// 获取整行数据
$data = $db->query( 'SELECT binary FROM data;' )->fetchAll( PDO::FETCH_COLUMN );

// 与原始数组比较,注意任何不匹配的地方
for( $i = 0; $i < $num_values; $i++ )
if(
$data[$i] != $binary[$i] ) echo "[$i] mismatch\n";

$db = null;

?>
1
php dot chaska at xoxy dot net
11 年前
请注意,对于 Postgres,即使 Postgres 确实支持预处理语句,PHP 的 PDO 驱动程序也 **从不** 在调用 PDO::execute() 之前将预处理语句发送到 Postgres 服务器。

因此,PDO::prepare() 永远不会因为诸如错误的 SQL 语法之类的问题而抛出错误。

这也意味着服务器在第一次调用 PDO::execute() 之前不会解析和计划 SQL,这可能会也可能不会对你的优化计划产生不利影响。
2
pbakhuis
10 年前
在我看来,值得注意的是,如果你准备了一个语句但没有将值绑定到标记,它将默认插入 null。例如:
<?php
/** @var PDO $db */
$prep = $db->prepare('INSERT INTO item(title, link) VALUES(:title, :link)');
$prep->execute();
?>
将尝试向 item 表中插入 null, null。
0
machitgarha at outlook dot com
7 年前
大家好。

我想指出的是,无论你在哪里直接在查询中使用变量,都不能防止 SQL 注入(除非执行长时间的安全操作)。

以下示例容易受到 SQL 注入的攻击

<?php

$statement
= $databaseConnection->prepare("SELECT * FROM `$_POST['table']` WHERE $_POST['search_for']=:search");
$statement->bindParam(":search", $search);
$search = 18; // 例如
$statement->execute();

?>

如果攻击者将 '1;-- ' 作为名为 'search_for' 的输入传递,他还算不上一个非常坏的攻击者;因为他没有删除你的数据!在上面的示例中,攻击者可以使用连接的数据库执行任何操作(除非你限制了连接用户的权限)。不幸的是,正如 Simon Le Pine 所提到的,你不能将预处理语句用作查询的其他部分;只能用于在索引中搜索。

希望这有助于避免丢失一些数据。
抱歉,我的英语有点差!
1
roth at egotec dot com
18 年前
使用 MySQL 和预处理语句时请注意。
在一个语句中多次使用占位符不起作用。PDO 只会转换第一个出现的占位符,而将第二个占位符保留原样。

select id,name from demo_de where name LIKE :name OR name=:name

你必须使用

select id,name from demo_de where name LIKE :name OR name=:name2

并绑定两次 name。我不知道其他数据库(例如 Oracle 或 MSSQL)是否支持多次出现。如果是这样,那么应该更改 PDO 对 MySQL 的行为。
-2
orrd101 at gmail dot com
12 年前
不要仅仅自动地对所有查询使用 prepare()。

如果你只提交一个查询,使用 PDO::query() 和 PDO::quote() 会快得多(在我的 MySQL 测试结果中大约快 3 倍)。只有当你一次提交数千个相同的查询(使用不同的数据)时,预处理查询才会更快。

如果你在 Google 上搜索性能比较,你会发现通常情况都是如此,或者你可以编写一些代码并针对你的特定配置和查询场景进行自己的比较。但通常情况下,PDO::query() 总是更快,除非提交大量相同的查询。预处理查询确实具有为你转义数据的优点,因此在使用 query() 时必须确保使用 quote()。
-2
Hayley Watson
11 年前
可以预先准备针对单个连接的多个语句。只要该连接保持打开状态,就可以根据需要按任何顺序执行和获取这些语句;它们的“准备-执行-获取”步骤可以以最佳方式交错进行。

因此,如果你可能经常使用多个语句(可能在事务循环中),你可能需要考虑预先准备好你将要使用的所有语句。
-3
ak_9jsz
16 年前
使用游标在 SQLite 3.5.9 中不起作用。当执行到 execute() 方法时,我收到一条错误消息。

你们中的一些人可能会说“废话!”,但我很惊讶地看到 SQLite 中支持 TRIGGER,所以我不得不尝试一下。:)

我想在可滚动游标上使用绝对引用,并且我只需要一列数据。所以我用了这个代替游标。

<?php

$dbo
= new PDO('sqlite:tdb');
$sql = 'SELECT F1, F2 FROM tblA WHERE F1 <> "A";';
$res = $dbo->prepare($sql);
$res->execute();
$resColumn = $res->fetchAll(PDO::FETCH_COLUMN, 0);

for(
$r=0;$r<=3;$r++)
echo
'第 '. $r . ' 行返回: ' . $resColumn[$r] . "\n";

$dbo = null;
$res = null;
?>
-4
sgirard at rossprint dot com
15 年前
也许其他人都已经知道了,但是...

如果您有一个例程,为 sqlite 数据库准备/执行许多插入或更新语句,那么您可能需要使用 pdo 事务。

在一些旧硬件上,我的查询集从 12 秒缩短到 1/3-1/2 秒。

-sean
To Top