PDO (PHP Data Objects) 是 PHP 5.1+ 內建的資料庫抽象層,支援 12 種以上資料庫驅動(MySQL、PostgreSQL、SQLite、SQL Server、Oracle 等),提供**統一介面、預處理語句防 SQL 注入、事務控制、異常處理**等現代特性。本文系統整理 PDO 核心用法,含連線配置、查詢執行、結果集獲取模式、預處理參數綁定、事務管理、錯誤處理兩大模式、常見陷阱與最佳實踐,適合作為開發手邊速查手冊。
<?php
// MySQL 範例
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4';
$username = 'root';
$password = '';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 拋出異常
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 預設回傳關聯陣列
PDO::ATTR_EMULATE_PREPARES => false, // 使用原生預處理
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4", // 連線後執行
];
try {
$pdo = new PDO($dsn, $username, $password, $options);
} catch (PDOException $e) {
error_log("DB Connection Failed: " . $e->getMessage());
die("資料庫連線失敗,請稍後再試");
}
?>
常見 DSN 格式:
mysql:host=HOST;dbname=DB;charset=utf8mb4pgsql:host=HOST;dbname=DBsqlite:/path/to/db.sqlitesqlsrv:Server=HOST;Database=DBoci:dbname=//HOST:PORT/SERVICE| 方法 | 用途 | 回傳值 | 典型場景 |
|---|---|---|---|
query($sql) | 執行 SELECT 等有結果集的語句 | PDOStatement 物件 | 簡單查詢、無參數綁定 |
exec($sql) | 執行 INSERT/UPDATE/DELETE/DDL 等無結果集語句 | 影響行數 int | 單次寫入、建表、索引 |
prepare($sql) | 預處理語句,回傳 PDOStatement,需再呼叫 execute() | PDOStatement | **所有帶參數查詢、批次執行、防注入** |
query()$stmt = $pdo->query("SELECT id, name, email FROM users WHERE status = 1");
foreach ($stmt as $row) {
echo $row['name'] . ' <' . $row['email'] . '>
';
}
exec()$count = $pdo->exec("INSERT INTO logs (msg, created_at) VALUES ('system start', NOW())");
echo "寫入 $count 筆";
prepare() + execute()(核心推薦)$stmt = $pdo->prepare("INSERT INTO users (name, email, role) VALUES (?, ?, ?)");
$stmt->execute(['Alice', 'alice@example.com', 'admin']); // 位置佔位符
// 或具名佔位符(可讀性更好)
$stmt = $pdo->prepare("INSERT INTO users (name, email, role) VALUES (:name, :email, :role)");
$stmt->execute([
':name' => 'Bob',
':email' => 'bob@example.com',
':role' => 'user',
]);
$stmt = $pdo->query("SELECT id, name, role FROM users");
// 1. FETCH_ASSOC(預設若設 ATTR_DEFAULT_FETCH_MODE)
$stmt->setFetchMode(PDO::FETCH_ASSOC);
while ($row = $stmt->fetch()) { /* $row['name'] */ }
// 2. FETCH_OBJ → 物件屬性存取
$stmt->setFetchMode(PDO::FETCH_OBJ);
while ($obj = $stmt->fetch()) { echo $obj->name; }
// 3. FETCH_CLASS → 直接映射到自訂類別(需類別屬性為 public 或有 __set)
class User { public $id; public $name; public $role; }
$stmt->setFetchMode(PDO::FETCH_CLASS, 'User');
while ($user = $stmt->fetch()) { $user->sayHello(); }
// 4. FETCH_INTO → 映射到現有物件實例
$user = new User();
$stmt->setFetchMode(PDO::FETCH_INTO, $user);
while ($stmt->fetch()) { /* $user 更新 */ }
// 5. FETCH_COLUMN → 只取單一欄位(索引 0-based)
$stmt = $pdo->query("SELECT name FROM users");
$names = $stmt->fetchAll(PDO::FETCH_COLUMN, 0); // ['Alice','Bob',...]
// 6. FETCH_KEY_PAIR → 第一欄為鍵、第二欄為值
$map = $pdo->query("SELECT id, name FROM users")->fetchAll(PDO::FETCH_KEY_PAIR);
// [1=>'Alice', 2=>'Bob', ...]
// 7. FETCH_GROUP → 依第一欄分組
$grouped = $pdo->query("SELECT role, name FROM users")->fetchAll(PDO::FETCH_GROUP);
// ['admin'=>[['name'=>'Alice']], 'user'=>[['name'=>'Bob']], ...]
$stmt = $pdo->prepare("INSERT INTO logs (level, message) VALUES (?, ?)");
$data = [
['error', 'Disk full'],
['warn', 'High memory'],
['info', 'Backup done'],
];
foreach ($data as $row) {
$stmt->execute($row);
}
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("INSERT INTO big_table (data) VALUES (?)");
foreach (range(1, 10000) as $i) {
$stmt->execute(["chunk-$i"]);
if ($i % 500 === 0) $pdo->commit(); // 可選:分段提交避免長鎖
$pdo->beginTransaction();
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
PDO::ATTR_EMULATE_PREPARES => false 強制原生預處理防止驅動層模擬預處理導致的編碼/注入問題(MySQL 預設會模擬)。
$pdo->beginTransaction();
try {
$pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$pdo->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
error_log("Transfer failed: " . $e->getMessage());
throw $e;
}
// 檢查是否在事務中
if ($pdo->inTransaction()) { /* ... */ }
注意:MySQL MyISAM 不支援事務,需使用 InnoDB;DDL 語句 (CREATE/ALTER/DROP) 會隱性提交事務。
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
$pdo->query("SELECT * FROM non_existent_table");
} catch (PDOException $e) {
// PDOException 繼承 Exception,擴充屬性:
// $e->getMessage() // 錯誤訊息
// $e->getCode() // SQLSTATE 代碼 (如 '42S02')
// $e->errorInfo // 陣列 [SQLSTATE, Driver Code, Driver Message]
error_log("SQL Error: {$e->getMessage()} | Code: {$e->getCode()}");
// 可依 code 判斷:23000=完整性約束違反、42000=語法錯誤、HY000=一般錯誤
throw new RuntimeException("資料庫操作失敗", 500, $e);
}
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); // 預設
$stmt = $pdo->query("SELECT * FROM users");
if ($pdo->errorCode() !== '00000') {
$err = $pdo->errorInfo(); // [SQLSTATE, Driver Code, Message]
error_log("Query failed: {$err[2]} (SQLSTATE: {$err[0]})");
die("查詢失敗");
}
// 或檢查 Statement
if ($stmt->errorCode() !== '00000') { /* ... */ }
Array
(
[0] => '42S22' // SQLSTATE
[1] => 1054 // MySQL 錯誤代碼
[2] => 'Unknown column 'aaa' in field list' // 驅動器訊息
)
| 方法 | 說明 |
|---|---|
lastInsertId([$seqName]) | 取得最後一次 INSERT 的 AUTO_INCREMENT ID(PostgreSQL 需傳 sequence 名) |
rowCount() | 上次 DELETE/INSERT/UPDATE 影響行數(**SELECT 不可靠**,請用 COUNT(*)) |
quote($str) | 字串轉義並加引號(極少直接用,預處理已涵蓋) |
getAttribute(PDO::ATTR_DRIVER_NAME) | 取得驅動名稱 (mysql, pgsql, sqlite...) |
setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER/NATURAL/UPPER) | 控制欄位名大小寫 |
$order = in_array($_GET['order'], ['id','name','created']) ? $_GET['order'] : 'id';
$stmt = $pdo->prepare("SELECT * FROM users ORDER BY $order");
$ids = [1,2,3];
$place = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($place)");
$stmt->execute($ids);
PDO::ATTR_PERSISTENT => true 可減少連線開銷,但需注意:
PDO::ATTR_PERSISTENT 專用連線池中介軟體 (ProxySQL, PgBouncer)。charset=utf8mb4 + PDO::MYSQL_ATTR_INIT_COMMAND 雙重保障,避免 Emoji 存入亂碼。fetch() 迴圈逐行處理,或用無緩衝查詢 PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false(需謹慎)。<?php
class UserRepository {
private $pdo;
public function __construct(PDO $pdo) { $this->pdo = $pdo; }
public function findById(int $id): ?array {
$stmt = $this->pdo->prepare("SELECT id, name, email, role FROM users WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
public function findByEmail(string $email): ?array {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
public function create(string $name, string $email, string $role = 'user'): int {
$stmt = $this->pdo->prepare("INSERT INTO users (name, email, role, created_at) VALUES (?, ?, ?, NOW())");
$stmt->execute([$name, $email, $role]);
return (int)$this->pdo->lastInsertId();
}
public function updateRole(int $id, string $role): bool {
$stmt = $this->pdo->prepare("UPDATE users SET role = ? WHERE id = ?");
return $stmt->execute([$role, $id]);
}
public function delete(int $id): bool {
return $this->pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$id]);
}
public function search(string $keyword, int $limit = 20, int $offset = 0): array {
$stmt = $this->pdo->prepare("SELECT id, name, email FROM users WHERE name LIKE ? OR email LIKE ? LIMIT ? OFFSET ?");
$kw = "%$keyword%";
$stmt->bindValue(1, $kw, PDO::PARAM_STR);
$stmt->bindValue(2, $kw, PDO::PARAM_STR);
$stmt->bindValue(3, $limit, PDO::PARAM_INT);
$stmt->bindValue(4, $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
?>
PDO 是 PHP 連接資料庫的**標準現代介面**,核心優勢在於**統一 API、原生預處理防注入、物件導向異常處理、靈活的獲取模式**。掌握 prepare() → execute() → fetch*() 流程、事務控制、錯誤模式切換、參數綁定細節,即能編寫安全、高效、可維護的資料存取層。建議專案統一封裝 Database::pdo() 單例或依賴注入容器管理連線,並制定「禁止字串拼接 SQL」編碼規範,配合 Code Review 與靜態分析工具落實執行。