預處理語句是資料庫操作的黃金標準,能徹底防禦 SQL 注入、提升重複執行效能、減少網路傳輸。本文詳細說明運作原理、MySQLi 與 PDO 兩套介面完整實作、參數綁定型別、交易控制、常見錯誤與最佳實務。
| 優勢 | 說明 |
| 防 SQL 注入 | 參數值與 SQL 碼分離,資料庫將參數視為純資料,非可執行碼 |
| 效能提升 | 重複執行僅傳送參數,減少解析優化、網路頻寬 |
| 型別安全 | 綁定時指定型別 (整數、字串、浮點、Blob),資料庫驗證 |
| 可讀性 | SQL 模板與資料邏輯分離 |
MySQLi 實作完整範例
物件導向風格
<?php
$host = 'localhost'; $user = 'username'; $pass = 'password'; $db = 'myDB';
$mysqli = new mysqli($host, $user, $pass, $db);
if ($mysqli->connect_error) {
die("連線失敗: " . $mysqli->connect_error);
}
// 1. 準備
$stmt = $mysqli->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
if (!$stmt) {
die("準備失敗: " . $mysqli->error);
}
// 2. 綁定參數
// "sss" = 三個字串參數
// 型別對應: i=integer, d=double, s=string, b=blob
$stmt->bind_param("sss", $firstname, $lastname, $email);
// 3. 重複執行
$data = [
['John', 'Doe', 'john@example.com'],
['Mary', 'Moe', 'mary@example.com'],
['Julie', 'Dooley', 'julie@example.com']
];
foreach ($data as $row) {
[$firstname, $lastname, $email] = $row;
if (!$stmt->execute()) {
echo "執行失敗: " . $stmt->error;
}
}
echo "新記錄插入成功";
$stmt->close();
$mysqli->close();
程序式風格
$conn = mysqli_connect($host, $user, $pass, $db);
if (!$conn) die("連線失敗: " . mysqli_connect_error());
$stmt = mysqli_stmt_init($conn);
if (mysqli_stmt_prepare($stmt, "INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)")) {
mysqli_stmt_bind_param($stmt, 'sss', $firstname, $lastname, $email);
$data = [ /* ... */ ];
foreach ($data as $row) {
[$firstname, $lastname, $email] = $row;
mysqli_stmt_execute($stmt);
}
}
mysqli_stmt_close($stmt);
mysqli_close($conn);
PDO 實作完整範例
<?php
$host = 'localhost'; $user = 'username'; $pass = 'password'; $db = 'myDBPDO';
try {
$pdo = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // 使用真實預處理
// 命名佔位符 (推薦,可讀性佳)
$stmt = $pdo->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (:firstname, :lastname, :email)");
// 綁定參數 (參考綁定,變數值改變即生效)
$stmt->bindParam(':firstname', $firstname);
$stmt->bindParam(':lastname', $lastname);
$stmt->bindParam(':email', $email);
$data = [
['John', 'Doe', 'john@example.com'],
['Mary', 'Moe', 'mary@example.com'],
['Julie', 'Dooley', 'julie@example.com']
];
foreach ($data as $row) {
[$firstname, $lastname, $email] = $row;
$stmt->execute();
}
echo "新記錄插入成功";
} catch (PDOException $e) {
echo "錯誤: " . $e->getMessage();
}
$pdo = null;
參數綁定型別對照表
| 型別字元 | PHP 型別 | MySQL 型別範例 |
i | int | TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT |
d | float | FLOAT, DOUBLE, DECIMAL |
s | string | CHAR, VARCHAR, TEXT, ENUM, SET, DATE, TIME, DATETIME, JSON |
b | string (二進位) | BLOB, BINARY, VARBINARY |
PDO::PARAM_* 常數
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->bindValue(':name', $name, PDO::PARAM_STR);
$stmt->bindValue(':data', $blob, PDO::PARAM_LOB);
$stmt->bindValue(':bool', $flag, PDO::PARAM_BOOL);
進階技巧與最佳實務
1. 批次插入優化 (單次交易)
// MySQLi
$mysqli->begin_transaction();
$stmt = $mysqli->prepare("INSERT INTO logs (message) VALUES (?)");
$stmt->bind_param('s', $msg);
foreach ($messages as $msg) $stmt->execute();
$mysqli->commit();
// PDO
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO logs (message) VALUES (?)");
foreach ($messages as $msg) {
$stmt->bindValue(1, $msg);
$stmt->execute();
}
$pdo->commit();
2. 動態建構 WHERE 條件
function buildWhere(array $filters, array &$params): string {
$clauses = [];
foreach ($filters as $field => $value) {
if (is_array($value)) {
// IN 查詢
$placeholders = implode(',', array_fill(0, count($value), '?'));
$clauses[] = "`$field` IN ($placeholders)";
$params = array_merge($params, $value);
} else {
$clauses[] = "`$field` = ?";
$params[] = $value;
}
}
return $clauses ? 'WHERE ' . implode(' AND ', $clauses) : '';
}
$filters = ['status' => 'active', 'category_id' => [1,2,3]];
$params = [];
$where = buildWhere($filters, $params);
// $where = "WHERE `status` = ? AND `category_id` IN (?,?,?)"
// $params = ['active', 1, 2, 3]
$stmt = $pdo->prepare("SELECT * FROM products $where");
$stmt->execute($params);
3. 獲取自動遞增 ID
// MySQLi
$stmt->execute();
$newId = $stmt->insert_id; // 或 $mysqli->insert_id
// PDO
$stmt->execute();
$newId = $pdo->lastInsertId();
4. 預處理語句與 LIKE 查詢
// 正確:萬用字元包含在參數值中
$search = "%$keyword%";
$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE ?");
$stmt->execute([$search]);
// 錯誤:SQL 中拼接萬用字元 (無法使用索引、注入風險)
// $stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE '%?%'");
5. 限制預處理語句快取大小
// MySQL 伺服器端快取限制
// max_prepared_stmt_count (預設 16382)
// 超過會導致 "Prepared statement needs to be re-prepared"
// 解決:增大參數、減少同時開啟的 stmt、使用後立即 close
常見錯誤與陷阱
| 錯誤 | 原因 | 修正 |
| 綁定參數數量不符 | 佔位符與 bind_param 數量不一致 | 仔細核對,使用命名佔位符減少位置錯誤 |
| 型別字元錯誤 | "s" 用於整數欄位 | 嚴格對應資料庫欄位型別 |
| PDO 模擬預處理 | ATTR_EMULATE_PREPARES=true (預設) | 設為 false 強制真實預處理 |
| 未檢查 prepare 回傳值 | SQL 語法錯誤導致 $stmt 為 false | 必須檢查 $stmt 非 false 再操作 |
| 交易中未處理例外 | 例外導致交易懸掛 | try-catch 中 rollback |
| 重複使用已關閉 stmt | $stmt->close() 後再 execute | 每次 prepare 新 stmt 或重新 prepare |
MySQLi vs PDO 預處理比較
| 特性 | MySQLi | PDO |
| 佔位符風格 | 僅 ? (位置) | ? 和 :name (命名) |
| 綁定方式 | bind_param (參考) | bindParam (參考) / bindValue (值) |
| 資料庫支援 | 僅 MySQL | 12+ 資料庫 |
| 真實預處理 | 永啟用 | 需關閉 ATTR_EMULATE_PREPARES |
| 例外處理 | 需手動檢查 | ERRMODE_EXCEPTION 自動拋出 |
| 多重結果集 | 支援 (next_result) | 不直接支援 |
總結
預處理語句是 PHP 連接 MySQL 的必要實務。新專案強烈建議使用 PDO + 真實預處理 + 命名佔位符 + 例外模式,獲得跨資料庫可移植性、型別安全、清晰錯誤處理。掌握參數綁定型別、動態條件建構、交易控制、批次優化等進階技巧,能構建安全、高效、可維護的資料存取層。
- 上一篇文章: PHP MySQL SELECT 查詢完整教學
- 下一篇文章: PHP MySQL 批次插入資料完全指南:多條 SQL、預處理語句與交易控制
Copyright ©
程式碼之家 All Rights Reserved