博客 / 詳情

返回

(唯一合適) PDO 教程

PDO是什麼

首先思考, 為什麼選擇PDO

PDO 是一個數據訪問抽象層(Database Access Abstraction Layer). 抽象是雙重的: 一個是眾所周知但不太重要的. 另一個是模糊的但是是最重要的.
眾所周知 PDO 為不同的數據庫提供了統一的接口. 雖然這個功能本身很龐大, 但是對於固定程序來説不是過於重要的事情, 基本所有的程序都是使用統一的後端數據庫. 儘管有一些謠言, 但是通過改變單行 PDO 配置來切換後端數據庫是不可能的-由於不同的 SQL 風格(為此, 需要使用像 DQL 這樣的平均查詢語言). 因此對於普通的 LAMP 開發者來説, 這一點是微不足道的, 並且對他而言, PDO只是熟悉的 mysql(i)_query() 函數的另一個更復雜版本. 但實際上它不是, 它有豐富的其他功能.
PDO 不僅抽象了數據庫API, 還抽象了基本操作, 否則必須在每個應用程序中重複數百次, 使您的代碼非常WET. 不同於 mysqlmysqli , 兩個都不能直接使用低級裸 APIs(但僅作為某些更高級別抽象層的構建材料), PDO就是這樣的抽象. 雖然仍是不完整的, 但是至少可用.
真正的PDO好處是:

  • 安全性 (可用的準備語句)
  • 可用性 (許多輔助函數可以自動執行日常操作)
  • 可重用性 (用於訪問大量數據庫的統一API, 從SQLITE到oracle)

請注意, 儘管 PDO 是原生數據庫驅動程序中最好的, 但對於現代WEB應用程序來説, 請考慮將使用有查詢構建器的 ORM 或者與其他更高抽象級別的庫一起使用, 只是偶爾使用原生的PDO. 好的ORM比如 Doctrine, Eloquent, RedBeanYii::AR. Aura.SQL 是具有很多附加功能的使用PDO包裝器的一個很好的例子.
無論哪種方式, 首先要了解基本工具是件好事. 那麼, 讓我們開始吧:

connection DSN

PDO有一個叫 DSN 的預想接方式. 它並不複雜-PDO需要你在三個不同的位置輸入不同的配置, 而不是一個簡單的選項列表.

  • database driver, host, db(schema) namecharset, 以及不常使用的 portunix_socket 設置 DSN
  • user_namepassword 設置構造方法
  • 其他所有的配置在options數組

其中 DSN 是以分號分隔的字符串, 由 param=value 鍵值對組成, 從驅動程序名稱和冒號開始:

    mysql:host=localhost;dbname=test;port=3306;charset=utf8mb4
driver^  ^colon           ^param=value pair   ^semicolon

注意, 遵循正確的格式是非常重要的- DSN中不能使用 空格, 引號, 和其他的符號, 只能使用參數, 值和定界符. 就像手冊上展示的.

這裏有一個例子:

$host = '127.0.0.1';
$db = 'test';
$pass = 'root';
$charset = 'utf8mb4';

$dsn = "mysql:host={$host};dbnamej={$db};charset={$charset}";
$options = [
    PDO::ATR_ERRMODE => PDO::ERRMODE_EXECPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];
try {
    $pdo = new \PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
}

設置了所有上述變量屬性, 我們將在 $pdo 變量中得到一個正確的 PDO 實例.
使用舊mysql擴展用户重要通知

  1. 不同於 mysql_* 函數, 可以在代碼的任意位置使用, pdo 實例被存儲在一個變量中, 那就意味着只能在函數內部進行訪問. 因此, 必須通過函數參數傳遞或使用更高級的技術, 比如IOC容器.
  2. 連接只用創建一次. 不要在函數, 類構造函數創建連接, 否則, 會創建多個連接, 最終導致數據庫服務宕機. 因此必須創建唯一的 PDO 實例, 讓整個腳本使用.(適用於FPM模式)
  3. 通過DSN設置字符集是非常重要的-這是唯一正確的方式因為它會告訴PDO哪個字符集會被使用. 因此, 忘記通過 Query 運行SET NAMES 或者通過 PDO::MYSQL_ATTR_INIT_COMMAND. 只有當PHP版本過低時(低於5.3.6), 才可以使用 SET NAMES 查詢, 並且關閉仿真模式.

更多關於連接的內容可以在 連接MySQL查看

運行查詢 PDO::query()

使用 PDO 有兩種方式運行查詢. 如果查詢中沒有使用變量, 可以使用 PDO::query 方法. 它會運行查詢並返回一個 PDOStatement 類的對象, 該類與 mysql_query 返回的資源大致相同, 特別時從中獲取實際記錄的操作:

$stmt = $pdo->query('SELECT name FROM users');
while ($row = $stmt->fetch()) {
    echo $row['name'] . "\n";
}

並且 query() 方法允許我們使用一個整潔的方法連接 SELECT 查詢, 如下所示.

預處理, 防止SQL注入

放棄熟悉的 mysql_query()函數 並進入嚴格數據對象領域的主要原因是 PDO 已經準備好了開箱即用的預處理語句. 如果要在語句中使用變量, 預處理語句是唯一正確運行的方式. 它如此重要的原因在 The Hitchhiker's Guide to SQL Injection prevention.有詳細的解釋.
對於運行的查詢, 如果至少使用一個變量, 你必須使用佔位符替換它. 準備執行語句, 然後分別傳入變量執行.
長話短説, 它不像感覺的那麼困難. 在大多數例子中, 你只需要使用函數 prepareexecute .
首先, 需要修改查詢, 在使用變量的位置添加佔位符, 就像這樣

$sql = "SELECT * FROM users WHERE email = '{$email}' AND status = '{$status}'";

改為

$sql = "SELECT * FROM users where email = ? and status = ?";

或者

$sql = "SELECT * FROM users where email = :email AND status = :status";

注意 PDO 支持位置(?)和命名(:email)佔位符, 後者始終以冒號開始,並且只能使用字母, 數字和下劃線. 還需要注意 佔位符周圍不能使用引號 .
一個查詢使用了佔位符, 就必須使用PDO::prepare()方法預處理. 這個方法返回一個和我們上邊討論的相同的 PDOStatement 對象, 但是沒有綁定任何數據.
最後, 必須使用 PDOStatement 對象的 execute() 方法執行查詢, 並且通過數組形式傳遞參數. 之後, 就可以從語句中得到結果數據(如果適用).

$stmt = $pdo->prepare("SELECT * FROM users WHERES email = ? AND status = ?");
$stmt->execute([$email, $status])
$user = $stmt->fetch();

// or
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email AND status = :status");
$stmt->execute(['email' => $email, 'status' => $status]);
$user = $stmt->fetch();

可以看到, 位置佔位符, 你需要提供一個索引數組. 命名佔位符, 需要提供一個關聯數組, 並且鍵要匹配查詢中的佔位符. 同一個查詢中不能混合位置佔位符和命名佔位符.
位置佔位符可以讓你寫更簡短的代碼, 但是對參數順序是敏感的(必須於查詢中參數的順序一致). 雖然命名佔位符使代碼更冗長, 但是允許隨機參數綁定.
另外需要注意, 雖然存在普遍的誤解, 但是數組鍵中 : 不是必須的.
執行後就可以使用支持的方法獲取結果.

更多的例子可以查看(respective article)[https://phpdelusions.net/pdo_...].

參數綁定

將數據傳入 execute() (如上所示)方法中應被視為默認的最方便的方式. 如果使用這個方法, 所有參數都將會綁定為字符串(如果使用NULL值, 將會使用SQL NULL發送給查詢), 大多數時候都沒有問題.
但是, 有時候最好明確設置類型. 可能情況如下:

  • 開啓仿真模式的 LIMIT 子句(或者其他不能接受字符串操作數的 SQL 子句)
  • 可能受到錯誤操作數類型影響具有特殊查詢計劃的複雜特殊查詢
  • 特有的列類型, 像 bigint boolean 必須綁定精確的操作數(為了將 BIGINT 綁定為 PDO::PARAM_INT 需要基於 mysqlnd)

這種情況下, 必須使用顯式綁定, 可以從 bindvalue()bindParam() 兩個函數中選擇一個. 前者是推薦使用的, 它不像 bindParam()具有一定的副作用.

查詢可以綁定的部分

瞭解哪些查詢部分可以使用參數綁定哪些部分不能使用是非常重要的. 事實上, 這個列表是非常短的: 只有字符串和數字字面量可以被綁定. 只要你的數據在查詢中能被表示為數字或者帶引號的字符串, 就可以被綁定. 其他所有情況你不能使用 PDO 預處理語句: 既不是標識符也不是逗號分隔列表, 或者是引用的文字字符串的一部分, 或者其他任意查詢部分都不能使用預準備語句綁定
最常見的用例解決方案可以在[本章的響應部分查看]()

預處理, 多次執行

有時候你可以使用預處理多次執行準備好的查詢, 比一次又一次執行相同的查詢快一點, 因為它只解析查詢一次. 如果可以執行另一個PHP實例中的預處理語句, 這個功能就是非常有用的, 但是事實並非如此. 只會在同一個實例中重複相同的查詢, 這在常規的PHP腳本中很少使用到, 並限制了此功能用於重複插入和更新.

$data = [
    1 => 1000,
    2 => 200,
    3 => 200,
];
$stmt = $pdo->prepare('UPDATE users SET bonus = bonus + ? where id = ?');
foreach ($data as $id => $bonus) {
    $stmt->execute([$bonus, $id]);
}

注意這個功能有點被高估了. 不僅需要討論, 而且性能提升也不是很大 - 查詢解析有時候是
很快的. 而且只有在關閉仿真模式的時候才能帶來性能提升.

運行SELECT INSERT UPDATE DELETE語句

這些查詢沒有什麼特別之處, 對PDO來説他們都是一樣的. 運行哪個查詢並不重要.
如上所示, 需要準備帶有佔位符的預處理查詢, 傳入變量並執行. DELETESELECT 的處理過程是基本相同的. 僅有的不同點是( DML查詢不會返回任何數據), 你可以使用鏈式方法, 調用 execute()prepare().

$sql = "UPDATE users SET name = ? where id = ?";
$pdo->prepare($sql)->execute([$name, $id]);

然而, 你像獲得影響行數, 代碼將和無聊的三行代碼相同:

$stmt = $pdo->prepare("DELETE FROM goods where category = ?");
$stmt->execute([$cat]);
$deleted = $stmt->rowCount();

更多的例子可以在respective article.找到.

從statement獲取數據 foreach

我們已經見過這個函數了, 現在讓我們仔細看看. 它從數據庫獲取單行數據, 在結果集中移動內部指針, 因此, 對函數的後續調用將逐個返回所有行. 這個方法和 mysql_fetch_array() 大致相同但在工作模式稍微有點不同: 代替很多不同函數( mysql_fetch_assoc() mysql_fetch_row), 這個只有一個方法, 但是它的行為可以通過一個參數改變. 在 PDO 中有很多的獲取模式, 稍後我們詳細討論, 這裏有一些簡單的實例:

  • PDO::FETCH_NUM 返回索引數組
  • PDO::FETCH_ASSOC 返回關聯數組
  • PDO::FETCH_BOTH 以上兩者都包含
  • PDO::FETCH_OBJ 返回對象
  • PDO::FETCH_LAZY 允許三個(索引數組, 關聯數組, 對象)方法沒有內存開銷.

從上面可以看出, 這個必須在兩種情況下使用:

  1. 當只需要一行時, 只獲取一行

        $row = $stmt->fetch(PDO::FETCH_ASSOC);

    將以關聯數組的方式從語句中獲取一行

  2. 當我們需要在使用之前處理返回數據. 在這種情況下, 必須通過while循環運行, 如上所示.

另一種有用的模式是 PDO::FETCH_CLASS 可以創建一個特定類的對象

$news = $pdo->query("select * from news")->fetchAll(PDO::FETCH_CLASS, 'News');

將生成一個News類對象的數組, 並且通過返回值設置類屬性. 注意這個模式下:

  • 屬性會在構造方法之前設置
  • 所有未定義的屬性都會調用 __set魔術方法
  • 如果沒有 __set方法, 將會創建新屬性
  • 私有屬性也會被設置, 這有點意外但是非常方便
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.