PDO (PHP Data Objects) 是 PHP 5.1+ 引入的資料庫存取抽象層,提供統一介面操作多種資料庫。本文系統梳理 PDO 核心 API、預處理機制、事務控制、錯誤處理及生產環境優化配置,助你寫出安全、高效、可維護的資料庫代碼。
| 方法/屬性 | 返回值 | 適用場景 | 注意事項 |
|---|---|---|---|
PDO::exec() | int (影響行數) | INSERT/UPDATE/DELETE 等無結果集操作 | 返回 0 表示無影響行,非失敗;失敗返回 false |
PDOStatement::execute() | bool (成功/失敗) | 預處理語句執行 | 需配合 rowCount() 獲取影響行數 |
PDO::query() | PDOStatement | SELECT 等返回結果集操作 | 每次調用都會解析 SQL,不適用重複執行 |
PDO::prepare() | PDOStatement | 需重複執行、含用戶輸入的 SQL | 防 SQL 注入核心,支持命名/問號佔位符 |
<?php
// config/database.php
return [
'dsn' => 'mysql:host=127.0.0.1;dbname=myapp;charset=utf8mb4',
'username' => 'app_user',
'password' => 'strong_random_password',
'options' => [
// 核心安全/性能選項
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 拋出異常而非靜默失敗
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 默認返回關聯數組
PDO::ATTR_EMULATE_PREPARES => false, // 禁用模擬預處理,使用真實預處理
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, // 緩衝查詢,釋放服務器資源
PDO::ATTR_STRINGIFY_FETCHES => false, // 保持原生類型(整數/浮點不轉字符串)
PDO::ATTR_PERSISTENT => false, // 生產環境建議關閉持久連接,避免連接洩露
],
];
// 連接實例化
try {
$pdo = new PDO(
$config['dsn'],
$config['username'],
$config['password'],
$config['options']
);
} catch (PDOException $e) {
// 生產環境記錄日誌,不暴露敏感信息
error_log("DB Connection Failed: " . $e->getMessage());
die('服務暫時不可用,請稍後重試');
}
query() - 適用於無參數、單次執行<?php
$sql = 'SELECT id, username, email FROM users WHERE status = "active" ORDER BY created_at DESC LIMIT 10';
foreach ($pdo->query($sql) as $row) {
echo "{$row['id']}: {$row['username']} <{$row['email']}>
";
}
prepare() + bindParam() - 核心推薦模式<?php
// 單條插入/更新
$sql = 'UPDATE products SET name = :name, price = :price, updated_at = NOW() WHERE id = :id';
$stmt = $pdo->prepare($sql);
// 綁定參數:指定類型、長度,增強安全性
$stmt->bindParam(':name', $name, PDO::PARAM_STR, 100);
$stmt->bindParam(':price', $price, PDO::PARAM_STR); // 價格建議用字符串或 Decimal 類型
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$name = '新產品名稱';
$price = '199.00';
$id = 123;
$stmt->execute();
// 批量操作:準備一次,綁定執行多次
$insertSql = 'INSERT INTO logs (level, message, context, created_at) VALUES (:level, :msg, :ctx, NOW())';
$insertStmt = $pdo->prepare($insertSql);
$logs = [
['level' => 'info', 'msg' => '用戶登錄', 'ctx' => json_encode(['uid' => 1])],
['level' => 'warn', 'msg' => '密碼錯誤', 'ctx' => json_encode(['uid' => 1, 'ip' => '1.2.3.4'])],
];
foreach ($logs as $log) {
$insertStmt->bindParam(':level', $log['level'], PDO::PARAM_STR, 10);
$insertStmt->bindParam(':msg', $log['msg'], PDO::PARAM_STR, 255);
$insertStmt->bindParam(':ctx', $log['ctx'], PDO::PARAM_STR);
$insertStmt->execute();
}
<?php
$stmt = $pdo->prepare('SELECT * FROM orders WHERE user_id = :uid AND status IN (:s1, :s2) ORDER BY created_at DESC');
$stmt->bindParam(':uid', $userId, PDO::PARAM_INT);
$stmt->bindParam(':s1', $status1, PDO::PARAM_STR, 20);
$stmt->bindParam(':s2', $status2, PDO::PARAM_STR, 20);
$userId = 456;
$status1 = 'paid';
$status2 = 'shipped';
$stmt->execute();
// 獲取模式對比
// 1. FETCH_ASSOC - 關聯數組(默認配置下)
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo "訂單 {$row['order_no']} 金額: {$row['amount']}
";
}
// 2. FETCH_OBJ - 匿名對象
$stmt->execute();
while ($obj = $stmt->fetch(PDO::FETCH_OBJ)) {
echo "訂單 {$obj->order_no} 金額: {$obj->amount}
";
}
// 3. FETCH_CLASS - 映射到實體類
class OrderEntity {
public $order_no;
public $amount;
public function getFormattedAmount() { return '¥' . number_format($this->amount, 2); }
}
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_CLASS, 'OrderEntity');
while ($order = $stmt->fetch()) {
echo "{$order->order_no}: {$order->getFormattedAmount()}
";
}
// 4. FETCH_KEY_PAIR - 鍵值對(需查詢僅兩列)
$stmt = $pdo->query('SELECT id, username FROM users LIMIT 5');
$map = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); // [1 => 'admin', 2 => 'user1', ...]
// 5. FETCH_GROUP - 分組
$stmt = $pdo->query('SELECT category, name FROM products');
$grouped = $stmt->fetchAll(PDO::FETCH_GROUP); // ['electronics' => [['name'=>'phone'],...], 'clothing' => [...]]
<?php
try {
$pdo->beginTransaction();
// 扣減庫存
$stmt = $pdo->prepare('UPDATE products SET stock = stock - :qty WHERE id = :id AND stock >= :qty');
$stmt->bindParam(':qty', $qty, PDO::PARAM_INT);
$stmt->bindParam(':id', $productId, PDO::PARAM_INT);
$qty = 1; $productId = 100;
$stmt->execute();
if ($stmt->rowCount() === 0) {
throw new Exception('庫存不足');
}
// 創建訂單
$stmt = $pdo->prepare('INSERT INTO orders (user_id, product_id, qty, total, status) VALUES (?, ?, ?, ?, "pending")');
$stmt->execute([$userId, $productId, $qty, $price * $qty]);
$orderId = $pdo->lastInsertId();
// 記錄操作日誌
$stmt = $pdo->prepare('INSERT INTO order_logs (order_id, action, operator) VALUES (?, "create", ?)');
$stmt->execute([$orderId, $userId]);
$pdo->commit();
echo "下單成功,訂單號:$orderId";
} catch (Exception $e) {
$pdo->rollBack();
error_log("Order failed: " . $e->getMessage());
echo "下單失敗:" . $e->getMessage();
}
關鍵點:
rollBack(),否則隱式提交導致數據不一致<?php
// 統一異常處理模式
try {
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
} catch (PDOException $e) {
// 區分錯誤類型
switch ($e->getCode()) {
case '23000': // 完整性約束違反
if (strpos($e->getMessage(), 'Duplicate entry') !== false) {
throw new BusinessException('郵箱已被註冊');
}
break;
case 'HY000': // 通用錯誤
// 可能是連接斷開、表不存在等
break;
}
error_log("SQL Error: {$e->getMessage()} | Code: {$e->getCode()} | SQL: SELECT * FROM users WHERE email = ?");
throw new SystemException('數據庫操作失敗');
}
// 調試:獲取最後執行的 SQL(需啟用 ATTR_EMULATE_PREPARES 或使用查詢日誌)
// 實際生產建議開啟 MySQL 慢查詢日誌或使用 ProxySQL 審計
ATTR_EMULATE_PREPARES => false)INSERT ... VALUES (),(),() 批次語句,或 LOAD DATA INFILE| 誤區 | 正確做法 |
|---|---|
| 以為 prepare 後直接拼接變量安全 | 必須使用 bindParam/bindValue 或 execute([...]) 綁定參數 |
| 所有查詢都用 prepare | 無參數、單次執行的查詢用 query() 更簡潔 |
| rowCount() 用於 SELECT 獲取行數 | 不可靠!SELECT 應用 COUNT(*) 查詢或 fetchAll() 後 count() |
| 捕獲 PDOException 後直接輸出 $e->getMessage() | 生產環境記錄日誌,用戶端返回友好提示,防信息洩露 |
| 長連接不關閉 | 腳本結束自動釋放,長運行進程需手動 $pdo = null 釋放 |
PDO 是 PHP 現代化資料庫開發的基石。掌握預處理機制、參數綁定、事務邊界、異常處理四大核心,配合合理的連接配置與監控體系,即可構建安全、高效、可維護的數據訪問層。建議在項目中封裝輕量級 Repository 基類,統一異常轉換、日誌記錄、審計字段填充等橫切關注點。