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

此数组保存一个或多个键=>值对,以设置此方法返回的 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();
?>

另请参阅

添加说明

用户贡献说明 26 个说明

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

人们普遍误解了准备好的语句中占位符的工作原理:它们并非简单地被替换为(转义的)字符串,然后执行生成的 SQL。相反,DBMS 被要求“准备”一条语句,会为如何执行该语句制定一个完整的查询计划,包括将使用哪些表和索引,这与如何填充占位符无关。

“SELECT name FROM my_table WHERE id = :value” 的计划与您用什么替换“:value” 相同,但看似类似的“SELECT name FROM :table WHERE id = :value” 无法被规划,因为 DBMS 不知道您实际上要从哪个表中进行选择。

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

当您的查询使用动态列引用时,您应该明确列出您知道存在于表中的列,例如使用 switch 语句,并在 default: 子句中抛出异常。
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 注入的可能性,因此可以使用我所做的方法安全地使用它。

希望这能帮助其他人免于大量搜索。
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 微秒。
虽然不多,但值得一提。
daniel dot egeberg at gmail dot com
15 年前
您还可以将值数组传递给 PDOStatement::execute()。这也能防止 SQL 注入。您不必使用 bindParam() 或 bindValue()。
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>';
}
admin at wdfa dot co dot uk
15 年前
关于准备好的语句的 SQL 注入属性的说明。

只有在使用 bindParam 或 bindValue 选项时,准备好的语句才能防止 SQL 注入。

例如,如果您有一个名为 users 的表,它有两个字段,用户名和电子邮件,而某人更新了他们的用户名,您可能会运行

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,那么会发生注入,电子邮件将被更新为 test,用户名也会被更新为 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。
Mark Simon
7 年前
许多学生倾向于在 SQL 语句中用单引号括起字符串占位符,因为他们在 SQL 和 PHP 中通常对字符串这样操作。

我必须解释

引号不是字符串的一部分——它们用于在编码语言中构建字符串。如果您在 SQL 或 PHP 中创建字符串文字,则它必须用引号引起来。如果字符串已创建,并且正在传递,那么额外的引号充其量是错误的,最坏的情况下会被误解。

在准备好的占位符中,将占位符视为变量,无论它们是字符串还是其他值,都始终不带引号书写。
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 对准备好的语句性能不佳。模拟工作速度明显更快。
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;

?>
pbakhuis
10 年前
我认为值得注意的是,如果您准备了一个语句但没有将值绑定到标记,它将默认插入 null。例如
<?php
/** @var PDO $db */
$prep = $db->prepare('INSERT INTO item(title, link) VALUES(:title, :link)');
$prep->execute();
?>
将尝试将 null,null 插入 item 表。
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 提到的,您不能将准备好的语句用作查询的其他部分;它只能用于索引搜索。

希望这有助于您避免丢失一些数据。
抱歉,我的英语有点弱!
php dot chaska at xoxy dot net
11 年前
请注意,对于 Postgres,即使 Postgres 支持准备好的语句,PHP 的 PDO 驱动程序也永远不会在调用 PDO::execute() 之前预先将准备好的语句发送到 Postgres 服务器。

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

这也意味着服务器直到第一次调用 PDO::execute() 才会解析和规划 SQL,这可能会或可能不会对您的优化计划产生不利影响。
roth at egotec dot com
17 年前
注意使用 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

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

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

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

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

你们中有些人可能会说“duh!”,但我对在 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
'Row '. $r . ' returned: ' . $resColumn[$r] . "\n";

$dbo = null;
$res = null;
?>
omidbahrami1990 at gmail dot com
6 年前
这是一个使用 pdo::prepare 安全登录的方法
--------------------------------------------------------
<?php
function secured_signin($username,$password)
{
try
{
$connection = new PDO("mysql:host=$dbhost;dbname=$dbname", $dbusername, $dbpassword);
$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$prepared = $connection->prepare("SELECT COUNT(`username`) FROM `users` WHERE `username` = :bp_username AND `password` = :bp_password ; ");
$prepared->bindParam(':bp_username', $username);
$prepared->bindParam(':bp_password', $password);
$prepared->execute();

if (
$prepared->fetchColumn() == 1)
$result=true;

else
$result=false;
}

catch(
PDOException $x) { die("Secured"); }

$prepared = null;
$connection = null;

return
$result;
}
/*
$dbhost ---> 数据库IP地址
$dbusername ---> 数据库用户名
$dbpassword ---> 数据库密码
$dbname ---> 数据库名称
*/
?>
jesse dot chisholm at gmail dot com
9年前
@Simon Le Pine

请注意

$search = "user";
$sth = db->prepare("SELECT * FROM users WHERE $search=:email");



$search = "email";
$sth = db->prepare("SELECT * FROM users WHERE $search=:email");

将生成两个完全不同的预处理语句。

这样做_不会起作用_

$search = "user";
$sth = db->prepare("SELECT * FROM users WHERE $search=:email");
$sth->execute(array(email=>"yada"));
$search = "email";
$sth->execute(array(email=>"[email protected]"));
sgirard at rossprint dot com
14 年前
也许其他人已经知道这一点,但是...

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

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

-sean
pascal dot buguet at laposte dot net
13年前
PDO::CURSOR_SCROLL 在 MSS 中可以使用。
您必须安装 PHP 2.0 CTP2 的 SQL Server 驱动程序:SQLSRV20.EXE
以及本机客户端“Microsoft SQL Server 2008 R2 本机客户端”:sqlncli.msi。
johniskew
17 年前
如果您需要在预处理语句中创建可变的 sql 语句...例如,您可能需要使用零个、一个、两个等数量的参数来构造 sql 查询...以下是一种无需大量 if/else 语句来粘合 sql 的方法

<?php

public function matchCriteria($field1=null,$field2=null,$field3=null) {
$db=DB::conn();
$sql=array();
$paramArray=array();
if(!empty(
$field1)) {
$sql[]='field1=?';
$paramArray[]=$field1;
}
if(!empty(
$field2)) {
$sql[]='field2=?';
$paramArray[]=$field2;
}
if(!empty(
$field3)) {
$sql[]='field3=?';
$paramArray[]=$field3;
}
$rs=$db->prepare('SELECT * FROM mytable'.(count($paramArray)>0 ? ' WHERE '.join(' AND ',$sql) : ''));
$result=$rs->execute($paramArray);
if(
$result) {
return
$rs;
}
return
false;
}

?>
william dot clarke at gmail dot com
17 年前
当然,如果您想以这种方式使用预处理语句,您应该使用第二个示例中的语法

例如。

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

使用
select id,name from demo_de where name LIKE ? OR name=?

我相信您应该使用不同的命名参数(name,name1)或匿名参数(?s)
richard at codevanilla.com
14 年前
小心
PDO 会为不支持原生准备语句/绑定参数的驱动程序模拟准备语句/绑定参数,并且还可以将命名或问号样式的参数标记重写为更合适的样式,如果驱动程序支持一种样式但不支持另一种样式。

这似乎包括 mySQL,因此

<?php
try{
$sth1 = $this->db1->prepare($t1, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));

}
catch(
PDOException $e){
return
$this->pack('dbError', $e->getMessage());
}
?>

不会,因此如果您的 SQL 语法错误,也不会抛出异常。

您需要检查 $sth1 是否不为空。
Kjetil H
11 年前
请注意,正确的内部方法签名是
<?php public function prepare ($statement, $driver_options = array()) ?>

而不是
<?php public function prepare ($statement, array $driver_options = array()) ?>.

使用后一种方法签名重新声明方法会抛出严格标准错误。
www.onphp5.com
17 年前
请注意,关于 driver_options 的声明具有误导性

“此数组包含一个或多个键=>值对,用于为该方法返回的 PDOStatement 对象设置属性值。您最常使用它将 PDO::ATTR_CURSOR 值设置为 PDO::CURSOR_SCROLL 以请求可滚动游标。一些驱动程序在准备时可能具有驱动程序特定的选项”

由此,您可能会认为可滚动游标适用于所有数据库,但事实并非如此!请查看此错误报告
http://bugs.php.net/bug.php?id=34625
chatelain dot cedric dot pro at gmail dot com
9年前
您不能使用预处理语句来创建数据库。

$sql = $conn->prepare("DROP DATABASE IF EXISTS :dbname ;",
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sql->execute(array(':dbname' => $dbname));

这将不起作用。
有人能解释一下吗?
To Top