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 則註解

匿名
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 發文,有點緊張。

經過一番搜尋,我了解了關於預處理語句的 2 件事
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 年前
佔位符字串存在限制。在以下程式碼中,第一個執行會失敗,並顯示 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 的表格,其中包含兩個欄位: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,就會發生 SQL Injection,而且 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'。
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 Injection 的風險。

模擬的原因是 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] 不符\n";

$db = null;

?>
php dot chaska at xoxy dot net
11 年前
請注意,對於 Postgres,即使 Postgres 支援預處理語句,PHP 的 PDO 驅動程式也絕不會在呼叫 PDO::execute() 之前將預處理語句傳送至 Postgres 伺服器。

因此,PDO::prepare() 永遠不會對像是錯誤的 SQL 語法之類的事情拋出錯誤。

這也表示伺服器會在第一次呼叫 PDO::execute() 時才剖析和規劃 SQL,這可能會或可能不會對您的最佳化計畫造成不良影響。
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 Injection 而言都是不安全的(除非執行長時間的安全性操作)。

以下範例對於 SQL Injection 而言是不安全的

<?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 所提到的,您無法將預處理語句用作查詢的其他部分,只能用於在索引中搜尋。

希望這有助於避免丟失一些資料。
抱歉,我的英文有點弱!
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

並將名稱繫結兩次。我不知道其他資料庫(例如 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() 方法時,我會收到錯誤訊息。

你們有些人可能會說「廢話!」,但我在 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;
?>
sgirard at rossprint dot com
15 年前
也許其他人都已經知道這一點,但是...

如果你有一個為 sqlite 資料庫準備/執行多個插入或更新陳述式的例程,那麼你可能需要使用 pdo 交易。

在一些舊硬體上,我的查詢集從 12 秒縮短到 1/3-1/2 秒。

-sean
To Top