PHP Data Objects (PDO) 提供統一的資料庫存取介面,支援 12 種資料庫驅動程式,是現代 PHP 專案的標準選擇。本文系統整理 PDO 核心方法、錯誤處理、交易控制與實戰技巧。
| 方法 | 用途 | 回傳值 |
|---|---|---|
PDO::query() | 執行 SELECT 等有結果集的查詢 | PDOStatement 物件 |
PDO::exec() | 執行 INSERT/UPDATE/DELETE 等無結果集操作 | 影響行數 (integer) |
PDO::lastInsertId() | 取得最後插入的自增 ID | 字串格式的 ID |
PDOStatement::fetch() | 逐行取得單筆記錄 | 陣列/物件/false |
PDOStatement::fetchAll() | 一次性取得所有記錄 | 二維陣列 |
PDOStatement::fetchColumn() | 直接取得指定欄位值 | 單一欄位值 |
PDOStatement::rowCount() | 取得 SELECT 影響行數 | 整數 |
PDOStatement::columnCount() | 取得結果集欄位數 | 整數 |
// 預設:同時回傳關聯與數字索引
$rows = $stmt->fetchAll(PDO::FETCH_BOTH);
// 僅關聯陣列 (最常用,記憶體效率佳)
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 僅數字索引陣列
$rows = $stmt->fetchAll(PDO::FETCH_NUM);
// 回傳物件 (屬性對應欄位名)
$rows = $stmt->fetchAll(PDO::FETCH_OBJ);
// 指定類別映射 (進階 ORM 雛形)
$stmt->setFetchMode(PDO::FETCH_CLASS, 'UserEntity');
while ($user = $stmt->fetch()) {
echo $user->getFullName();
}
$dsn = 'mysql:host=localhost;port=3306;dbname=test;charset=utf8mb4';
$options = [
PDO::ATTR_PERSISTENT => true, // 持久連線
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外模式
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 使用原生預處理
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
error_log("資料庫連線失敗: " . $e->getMessage());
die("系統暫時無法服務,請稍後再試");
}
ERRMODE_SILENT:預設值,靜默失敗,需手動檢查 errorInfo()ERRMODE_WARNING:觸發 E_WARNING,不中斷執行ERRMODE_EXCEPTION:推薦,拋出 PDOException,便於統一錯誤處理try {
$pdo->beginTransaction();
$pdo->exec("INSERT INTO `orders` (`user_id`, `amount`) VALUES (1001, 299.00)");
$orderId = $pdo->lastInsertId();
$pdo->exec("INSERT INTO `order_items` (`order_id`, `product_id`, `qty`) VALUES ($orderId, 50, 2)");
$pdo->exec("UPDATE `products` SET `stock` = `stock` - 2 WHERE `id` = 50");
$pdo->commit();
echo "訂單建立成功";
} catch (Exception $e) {
$pdo->rollBack();
error_log("訂單交易失敗: " . $e->getMessage());
throw $e;
}
// 命名參數 (PDO 專屬,可讀性高)
$sql = "SELECT * FROM `users` WHERE `email` = :email AND `status` = :status";
$stmt = $pdo->prepare($sql);
$stmt->execute([':email' => $email, ':status' => 1]);
// 位置參數 (相容性較好)
$sql = "SELECT * FROM `users` WHERE `email` = ? AND `status` = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$email, 1]);
// 批次插入優化
$sql = "INSERT INTO `logs` (`level`, `message`, `created_at`) VALUES (?, ?, NOW())";
$stmt = $pdo->prepare($sql);
foreach ($logEntries as $entry) {
$stmt->execute([$entry['level'], $entry['message']]);
}
// 明確指定資料類型
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->bindValue(':name', $name, PDO::PARAM_STR);
$stmt->bindValue(':data', $json, PDO::PARAM_LOB);
// 參考綁定 (適合迴圈大量資料)
$stmt->bindParam(':status', $status, PDO::PARAM_INT);
foreach ($statuses as $status) {
$stmt->execute();
}
class UserRepository {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function create(array $data): int {
$sql = "INSERT INTO `users` (`email`, `name`, `password_hash`) VALUES (:email, :name, :pass)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':email' => $data['email'],
':name' => $data['name'],
':pass' => password_hash($data['password'], PASSWORD_BCRYPT)
]);
return (int)$this->pdo->lastInsertId();
}
public function findByEmail(string $email): ?array {
$stmt = $this->pdo->prepare("SELECT * FROM `users` WHERE `email` = :email LIMIT 1");
$stmt->execute([':email' => $email]);
return $stmt->fetch() ?: null;
}
public function updateLastLogin(int $userId): bool {
$stmt = $this->pdo->prepare("UPDATE `users` SET `last_login` = NOW() WHERE `id` = :id");
return $stmt->execute([':id' => $userId]);
}
}
ATTR_EMULATE_PREPARES => false 使用資料庫原生準備語句lastInsertId() 在多筆 INSERT 中只返回第一筆 IDrowCount() 對 SELECT 在某些驅動不可靠,建議改用 COUNT(*)charset=utf8mb4 而非執行 SET NAMESwait_timeout 設定PDO 不只是資料庫抽象層,更是寫出安全、可維護、可攜遷 PHP 程式的基石。掌握預處理語句、交易控制、錯誤處理三大核心,即可應付 90% 的實務開發場景。建議將常用操作封裝成 Repository 模式,進一步提升程式碼品質。