2024 年日本 PHP 研討會

預備語句和預存程序

許多較成熟的資料庫都支援預備語句的概念。什麼是預備語句?可以將它們視為應用程式想要執行的 SQL 的一種已編譯範本,可以使用變數參數進行客製化。預備語句提供兩個主要優點:

  • 查詢只需要被解析(或準備)一次,但可以使用相同或不同的參數執行多次。當查詢被準備好時,資料庫將會分析、編譯和最佳化其執行查詢的計畫。對於複雜的查詢,這個過程可能需要相當長的時間,如果需要使用不同的參數多次重複相同的查詢,則會明顯降低應用程式的速度。透過使用預備語句,應用程式可以避免重複分析/編譯/最佳化循環。這表示預備語句使用較少的資源,因此執行速度更快。
  • 預備語句的參數不需要加上引號;驅動程式會自動處理。如果應用程式完全使用預備語句,開發人員可以確保不會發生 SQL 注入(然而,如果查詢的其他部分是用未逸出的輸入建構的,SQL 注入仍然可能發生)。

預備語句非常有用,以至於對於不支援它們的驅動程式,PDO 也會模擬這個功能。這確保了應用程式無論資料庫的功能如何,都能夠使用相同的資料存取模式。

範例 #1 使用預備語句重複插入

此範例透過將 `name` 和 `value` 替換為具名佔位符號來執行 INSERT 查詢。

<?php
$stmt
= $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");
$stmt->bindParam(':name', $name);
$stmt->bindParam(':value', $value);

// 插入一列資料
$name = 'one';
$value = 1;
$stmt->execute();

// 使用不同的值插入另一列資料
$name = 'two';
$value = 2;
$stmt->execute();
?>

範例 #2 使用預備語句重複插入

此範例透過將 `name` 和 `value` 替換為位置 `?` 佔位符號來執行 INSERT 查詢。

<?php
$stmt
= $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (?, ?)");
$stmt->bindParam(1, $name);
$stmt->bindParam(2, $value);

// 插入一列資料
$name = 'one';
$value = 1;
$stmt->execute();

// 使用不同的值插入另一列資料
$name = 'two';
$value = 2;
$stmt->execute();
?>

範例 #3 使用預備語句擷取資料

此範例根據表單提供的鍵值擷取資料。使用者輸入會自動被加上引號,因此沒有 SQL 注入攻擊的風險。

<?php
$stmt
= $dbh->prepare("SELECT * FROM REGISTRY where name = ?");
$stmt->execute([$_GET['name']]);
foreach (
$stmt as $row) {
print_r($row);
}
?>

範例 #4 使用輸出參數呼叫預存程序

如果資料庫驅動程式支援,應用程式也可以繫結輸出和輸入參數。輸出參數通常用於從預存程序擷取值。輸出參數的使用比輸入參數稍微複雜一些,因為開發人員在繫結參數時必須知道給定參數的大小。如果值大於建議的大小,則會引發錯誤。

<?php
$stmt
= $dbh->prepare("CALL sp_returns_string(?)");
$stmt->bindParam(1, $return_value, PDO::PARAM_STR, 4000);

// 呼叫預存程序
$stmt->execute();

print
"程序返回 $return_value\n";
?>

範例 #5 呼叫帶有輸入/輸出參數的預存程序

開發人員也可以指定同時保存輸入和輸出值的參數;語法與輸出參數類似。在接下來的例子中,字串 'hello' 被傳入預存程序,當它返回時,hello 會被程序的返回值取代。

<?php
$stmt
= $dbh->prepare("CALL sp_takes_string_returns_string(?)");
$value = 'hello';
$stmt->bindParam(1, $value, PDO::PARAM_STR|PDO::PARAM_INPUT_OUTPUT, 4000);

// 呼叫預存程序
$stmt->execute();

print
"程序返回 $value\n";
?>

範例 #6 佔位符號的無效用法

<?php
$stmt
= $dbh->prepare("SELECT * FROM REGISTRY where name LIKE '%?%'");
$stmt->execute([$_GET['name']]);

// 佔位符號必須用於整個值的取代
$stmt = $dbh->prepare("SELECT * FROM REGISTRY where name LIKE ?");
$stmt->execute(["%$_GET[name]%"]);
?>

新增筆記

使用者貢獻的筆記 3 則筆記

adam at pyramidpower dot com dot au
14 年前
請注意,當使用 bindParam 並以名稱作為參數時,名稱本身不能包含連字號「-」。

範例
<?php
$stmt
= $dbh->prepare ("INSERT INTO user (firstname, surname) VALUES (:f-name, :s-name)");
$stmt -> bindParam(':f-name', 'John');
$stmt -> bindParam(':s-name', 'Smith');
$stmt -> execute();
?>

「f-name」和「s-name」中的連字號應該用底線取代,或者完全不要使用連字號。

參見 http://bugs.php.net/43130

Adam
w37090 at yandex dot ru
4 年前
透過預備查詢將多維陣列插入資料庫
我們有一個用於寫入表單的陣列

$dataArr
陣列
(
[0] => 陣列
(
[0] => 2020
[1] => 23
[2] => 111111
)

[1] => 陣列
(
[0] => 2020
[1] => 24
[2] => 222222222
)
....

任務:準備一個請求並透過繫結傳遞
$array = [];
foreach ($dataArr as $k=>$v) {
// $x = 2020,變數是預先決定的,不改變本質
$array[] = [$x, $k, $v];
}
$sql = ("INSERT INTO `table` (`field`,`field`,`field`) VALUES (?,?,?)");

$db->queryBindInsert($sql,$array);

public function queryBindInsert($sql,$bind) {
$stmt = $this->pdo->prepare($sql);

if(count($bind)) {
foreach($bind as $param => $value) {
$c = 1;
for ($i=0; $i<count($value); $i++) {
$stmt->bindValue($c++, $value[$i]);
}
$stmt->execute();
}
}
}
theking2(at)king.ma
9 個月前
範例 #5 在 MariaDB 上測試時會產生 1414 錯誤。使用此函數可以呼叫預存程序,最後一個參數為 INOUT,並返回類似 (uu)id 或計數的值;

<?php
/**
* 呼叫指定的預存程序,並傳入給定的參數。
* 第一個參數是預存程序的名稱。
* 其餘參數是傳遞給預存程序的輸入參數。
* 最後一個輸出參數應為狀態或受影響列數等整數值。
*
* @param mixed $sp_name 要呼叫的預存程序名稱。
* @param mixed $params 要傳遞給預存程序的參數。
* @return int 受影響的列數。
*/
function call_sp( \PDO $db, string $sp_name, ...$params ): mixed
{
$placeholders = array_fill( 0, count( $params ), "?" );
$placeholders[] = "@new_id";

$sql = "CALL $sp_name( " . implode( ", ", $placeholders ) . " ); SELECT @new_id AS `new_id`";

try {
LOG->debug( "呼叫預存程序", [ "sql" => $sql ] );

$stmt = $db->prepare( $sql );
$i = 0;
foreach(
$params as $param ) {
$stmt->bindValue( ++$i, $param );
}
$stmt->execute();
$new_id = $stmt->fetch( PDO::FETCH_ASSOC )['new_id'];

return
$new_id;

} catch (
\Exception $e ) {
LOG->error( "呼叫預存程序時發生錯誤", [ "sql" => $sql, "params" => $params, "error" => $e->getMessage() ] );
throw
$e;
}
To Top