首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP PDO 完整使用指南:從連線、CRUD 到預處理、事務與錯誤處理

     編輯:PHP 相關知識

前言

PDO (PHP Data Objects) 是 PHP 5.1+ 內建的資料庫抽象層,支援 12 種以上資料庫驅動(MySQL、PostgreSQL、SQLite、SQL Server、Oracle 等),提供**統一介面、預處理語句防 SQL 注入、事務控制、異常處理**等現代特性。本文系統整理 PDO 核心用法,含連線配置、查詢執行、結果集獲取模式、預處理參數綁定、事務管理、錯誤處理兩大模式、常見陷阱與最佳實踐,適合作為開發手邊速查手冊。

一、建立連線 (DSN)

<?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: mysql:host=HOST;dbname=DB;charset=utf8mb4
  • PostgreSQL: pgsql:host=HOST;dbname=DB
  • SQLite: sqlite:/path/to/db.sqlite
  • SQL Server: sqlsrv:Server=HOST;Database=DB
  • Oracle: oci:dbname=//HOST:PORT/SERVICE

二、三大核心執行方法

方法用途回傳值典型場景
query($sql)執行 SELECT 等有結果集的語句PDOStatement 物件簡單查詢、無參數綁定
exec($sql)執行 INSERT/UPDATE/DELETE/DDL 等無結果集語句影響行數 int單次寫入、建表、索引
prepare($sql)預處理語句,回傳 PDOStatement,需再呼叫 execute()PDOStatement**所有帶參數查詢、批次執行、防注入**

1. 簡單查詢 query()

$stmt = $pdo->query("SELECT id, name, email FROM users WHERE status = 1");
foreach ($stmt as $row) {
    echo $row['name'] . ' <' . $row['email'] . '>
'; }

2. 寫入操作 exec()

$count = $pdo->exec("INSERT INTO logs (msg, created_at) VALUES ('system start', NOW())");
echo "寫入 $count 筆";

3. 預處理 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',
]);

三、結果集獲取模式 (Fetch Modes)

$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']], ...]

四、批次操作與效能優化

1. 批次 INSERT (單次 prepare、多次 execute)

$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);
}

2. 大量資料用 Transaction 包裹(關閉自動提交)

$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;
}

3. 使用 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) 會隱性提交事務。

六、錯誤處理兩大模式

模式 1:異常模式(推薦,現代標準)

$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);
}

模式 2:靜默模式(手動檢查)

$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') { /* ... */ }

errorInfo() 結構對照

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)控制欄位名大小寫

八、常見陷阱與避坑指南

  1. 預處理不能參數化「識別符」(表名、欄位名、ORDER BY 欄位),需經白名單驗證後拼接。
    $order = in_array($_GET['order'], ['id','name','created']) ? $_GET['order'] : 'id';
    $stmt = $pdo->prepare("SELECT * FROM users ORDER BY $order");
  2. IN 子句動態參數:需手動產生佔位符字串。
    $ids = [1,2,3];
    $place = implode(',', array_fill(0, count($ids), '?'));
    $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($place)");
    $stmt->execute($ids);
  3. 長連線 (Persistent):PDO::ATTR_PERSISTENT => true 可減少連線開銷,但需注意:
    • 連線池由進程管理,PHP-FPM 下每個 worker 獨立持有,可能導致連線數暴增。
    • 事務若未正確 commit/rollback,下一次請求復用連線時會殘留。
    • 建議**高併發短連線場景慎用**,或搭配 PDO::ATTR_PERSISTENT 專用連線池中介軟體 (ProxySQL, PgBouncer)。
  4. 字元集陷阱:DSN 加 charset=utf8mb4 + PDO::MYSQL_ATTR_INIT_COMMAND 雙重保障,避免 Emoji 存入亂碼。
  5. fetchAll() 記憶體暴增:大結果集請用 fetch() 迴圈逐行處理,或用無緩衝查詢 PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false(需謹慎)。

九、完整範例:UserRepository 類別

<?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 與靜態分析工具落實執行。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved