很多程式員都學習過如何使用 MySQL 或 MySQLi 擴充通路資料庫。在 PHP 5.1 中,有一個更好的方法。 PHP Data Objects (PDO) 提供了很多預處理語句的方法,且使用對象将使你的工作更有成效!
PDO 介紹
“PDO – PHP Data Objects – 是一個對多種資料庫提供統一操作方法的資料庫通路層。”
它并不具備資料庫特有的文法,但它将使切換資料庫和平台更加容易,多數情況下,隻需要簡單修改連結字元串。
這并非一篇完整教導如何使用SQL的教程。它重要為那些現今仍在使用 mysql 或 mysqli 擴充的人,幫助他們躍至更具可移植性和強力的 PDO。
資料庫支援
此擴充可以使用 PDO 驅動編寫過的所有資料庫。在本文書寫時,下面的資料庫支援已經實作:
- PDO_DBLIB ( FreeTDS / Microsoft SQL Server / Sybase )
- PDO_FIREBIRD ( Firebird/Interbase 6 )
- PDO_IBM ( IBM DB2 )
- PDO_INFORMIX ( IBM Informix Dynamic Server )
- PDO_MYSQL ( MySQL 3.x/4.x/5.x )
- PDO_OCI ( Oracle Call Interface )
- PDO_ODBC ( ODBC v3 (IBM DB2, unixODBC and win32 ODBC) )
- PDO_PGSQL ( PostgreSQL )
- PDO_SQLITE ( SQLite 3 and SQLite 2 )
- PDO_4D ( 4D )
你的系統不會也不必支援所有上面的驅動;下面是一個快速檢查所支援資料庫的方法:
1 | print_r(PDO::getAvailableDrivers()); |
連接配接
不同資料庫的連接配接方法可能稍有不同,下面是一些較為流行的資料庫連接配接方法。你将注意到,雖然資料庫類型不同,前三種資料庫的連接配接方式是相同的——而 SQLite 使用自己的文法。
02 | # MS SQL Server andSybase with PDO_DBLIB |
03 | $DBH = newPDO("mssql:host=$host;dbname=$dbname, $user, $pass"); |
04 | $DBH = newPDO("sybase:host=$host;dbname=$dbname, $user, $pass"); |
06 | # MySQL with PDO_MYSQL |
07 | $DBH = newPDO("mysql:host=$host;dbname=$dbname", $user, $pass); |
10 | $DBH = newPDO("sqlite:my/database/path/database.db"); |
12 | catch(PDOException $e) { |
注意 try/catch 塊——你應該總是使用 try/catch 包裝你的 PDO 操作,并使用異常機制——這裡隻是簡單的示例。通常,你隻需要一個連接配接——有很多可以教你文法的清單。 $DBH 代表“資料庫句柄”,這将貫穿全文。
通過将句柄設定為 NULL,你可以關閉任一連接配接。
你可以在PHP.net找到更多資料庫特定選項和/或其它資料庫連接配接字元串的資訊。
異常與 PDO
PDO 可以使用異常處理錯誤,這意味着你的所有 PDO 操作都應當包裝在一個 try/catch 塊中。你可以通過設定錯誤模式屬性強制 PDO 在建立的句柄中使用三種錯誤模式中的某一個。下面是文法:
1 | $DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); |
2 | $DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING ); |
3 | $DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); |
無論你設定哪個錯誤模式,一個錯誤的連接配接總會産生一個異常,是以建立連接配接應該總是包裝在 try/catch 塊中。
PDO::ERRMODE_SILENT
這是預設的錯誤模式。如果你使用這個模式,你将得使用同 mysql 或 mysqli 擴充一樣的方法差錯。其它兩種模式更适合 DRY 程式設計。
PDO::ERRMODE_WARNING
此方法将會發出一個标準PHP警告,并允許程式繼續運作。這對調試很有幫助。
PDO::ERRMODE_EXCEPTION
這是多數情況下你所希望的方式。它生成異常,允許你更容易的處理錯誤,隐藏可能導緻它人了解你系統的資訊。下面是一個充分利用異常的示例:
01 | # connect to the database |
03 | $DBH = newPDO("mysql:host=$host;dbname=$dbname", $user, $pass); |
04 | $DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); |
06 | # UH-OH! Typed DELECT instead of SELECT! |
07 | $DBH->prepare('DELECT name FROM people'); |
09 | catch(PDOException $e) { |
10 | echo"I'm sorry, Dave. I'm afraid I can't do that."; |
11 | file_put_contents('PDOErrors.txt', $e->getMessage(), FILE_APPEND); |
在 select 語句中有一個故意留下的錯誤;這将導緻一個異常。異常錯誤細節儲存至一個 log 檔案,并生成一段友好的(或不怎麼友好的)資訊於使用者。
插入和更新
插入新資料,更新已存資料是一種非常常見的資料庫操作。使用 PDO,這通常需要兩個步驟。本節中所述的所有内容對更新和插入都有效。
這裡有一個最基本的插入示例:
1 | # STH means "Statement Handle" |
2 | $STH = $DBH->prepare("INSERT INTO folks ( first_name ) values ( 'Cathy' )"); |
你也可以使用 exec() 完成相同的操作,這将減少調用。多數情況下,你會使用調用多的方法,以充分利用語句預處理的優勢。即使你隻用它一次,使用語句預處理,幫助你保護你的 SQL 免于注入攻擊。
預處理語句
使用語句預處理将幫助你免于SQL注入攻擊。
一條預處理語句是一條預編譯的 SQL 語句,它可以使用多次,每次隻需将資料傳至伺服器。其額外優勢在于可以對使用占位符的資料進行安全處理,防止SQL注入攻擊。
你通過在 SQL 語句中使用占位符的方法使用預處理語句。下面是三個例子:一個沒有占位符,一個使用無名占位符,一個使用命名占位符。
1 | # no placeholders - ripe for SQL Injection! |
2 | $STH = $DBH->("INSERT INTO folks (name, addr, city) values ($name, $addr, $city)"); |
5 | $STH = $DBH->("INSERT INTO folks (name, addr, city) values (?, ?, ?); |
8 | $STH = $DBH->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)"); |
你希望避免第一種方法。選擇命名我無名占位符将會對你對語句中資料的設定産生影響。
無名占位符
01 | # assign variables to each place holder, indexed 1-3 |
02 | $STH->bindParam(1, $name); |
03 | $STH->bindParam(2, $addr); |
04 | $STH->bindParam(3, $city); |
08 | $addr = "1 Wicked Way"; |
09 | $city = "Arlington Heights"; |
12 | # insert another row with different values |
14 | $addr = "5 Circle Drive"; |
這裡有兩步。首先,我們對各個占位符指定變量(2-4行)。然後,我們對各個占位符指定資料,并執行語句。要發送另一組資料,隻需改變這些變量的值并再次執行語句。
這種方法看上去對擁有很多參數的語句很笨拙吧?的确。然而,當資料儲存于數組中時,這非常容易簡略:
1 | # the data we want to insert |
2 | $data = array('Cathy', '9 Dark and Twisty Road', 'Cardiff'); |
4 | $STH = $DBH->("INSERT INTO folks (name, addr, city) values (?, ?, ?); |
容易吧!
數組中的資料按順序填入占位符中。 $data[0]是第一個,$data[1]是第二個,依次。不過,要是數組中資料的次序不正确,這将不能正常運作,你需要先對數組排序。
命名占位符
你可能已經開始猜測文法了,不過下面就是示例:
1 | # the first argument is the named placeholder name - notice named |
2 | # placeholders always start with a colon. |
3 | $STH->bindParam(':name', $name); |
你可以看使用快捷方式,但它需使用關聯數組。下面是示例:
1 | # the data we want to insert |
2 | $data = array( 'name' => 'Cathy', 'addr' => '9 Dark and Twisty', 'city' => 'Cardiff' ); |
5 | $STH = $DBH->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)"); |
數組中的鍵不需要以冒号開頭,但其它部分需要同占位符比對。如果你有一個二維數組,你隻需周遊它,并對周遊的每個數組執行語句。
命名占位符的另一個好的功能是直接将對象插入到你的資料庫中,隻要屬性同命名字段比對。下面是一個示例對象,以及如何将它插入到資料庫中的示例:
07 | function __construct($n,$a,$c) { |
15 | $cathy = new person('Cathy','9 Dark and Twisty','Cardiff'); |
17 | # here's the fun part: |
18 | $STH = $DBH->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)"); |
19 | $STH->execute((array)$cathy); |
通過在執行時将對象轉換為數組,輸将将會同數組的鍵一樣對待。
Selecting Data
資料通過語句句柄的->fetch() 方法擷取。在調用 fetch 之前,最好通知 PDO 你所希望擷取資料的方式。你有如下選項:
- PDO::FETCH_ASSOC:傳回一個通過字段名稱索引的數組。
- PDO::FETCH_BOTH (default):傳回一個數組,同時通過序号和名稱索引。
- PDO::FETCH_BOUND:通過->bindColumn() 方法綁定變量擷取傳回值
- PDO::FETCH_CLASS: 将傳回值配置設定給一個命名類。如果類比對屬性不存在,則将建立相應的屬性。
- PDO::FETCH_INTO: 更新一個命名類現有的執行個體化對象。
- PDO::FETCH_LAZY: 結合 PDO::FETCH_BOTH/PDO::FETCH_OBJ, 同它們各自方式一樣建立對象的變量名稱。
- PDO::FETCH_NUM:傳回一個按列順序數字索引的數組
- PDO::FETCH_OBJ:傳回一個匿名對象,屬性名稱對應列名。
在實際應用中,三個就能涵蓋大多數情況:FETCH_ASSOC、FETCH_CLASS 和 FETCH_OBJ。要設定 fetch 方法,使用如下文法:
1 | $STH->setFetchMode(PDO::FETCH_ASSOC); |
你也可以在調用 ->fetch() 方法時直接設定。
FETCH_ASSOC
這個 fetch 建立一個關聯數組,通過列的名稱索引。這對使用過 mysql/mysqli 擴充的人應該相當熟悉。下面是通過此方法擷取資料的示例:
01 | # using the shortcut ->query() method here since there are no variable |
02 | # values in the select statement. |
03 | $STH = $DBH->query('SELECT name, addr, city from folks'); |
05 | # setting the fetch mode |
06 | $STH->setFetchMode(PDO::FETCH_ASSOC); |
08 | while($row = $STH->fetch()) { |
09 | echo $row['name'] . "\n"; |
10 | echo $row['addr'] . "\n"; |
11 | echo $row['city'] . "\n"; |
while 循環将繼續逐行周遊結果集,直到周遊完畢。
FETCH_OBJ
此 fetch 将為傳回資料的每一行建立一個标準對象。示例如下:
01 | # creating the statement |
02 | $STH = $DBH->query('SELECT name, addr, city from folks'); |
04 | # setting the fetch mode |
05 | $STH->setFetchMode(PDO::FETCH_OBJ); |
08 | while($row = $STH->fetch()) { |
09 | echo $row->name . "\n"; |
10 | echo $row->addr . "\n"; |
11 | echo $row->city . "\n"; |
FETCH_CLASS
對象的屬性将在構造函數被調用之前完成設定,這點非常重要。
此 fetch 方法允許你将擷取結果直接填入你選擇的類中。當使用 FETCH_CLASS 時,對象的屬性将在構造函數被調用之前完成設定。再讀一遍,這點相當哪個重要。如果比對列名稱的屬性不存在,這些屬相将被建立(以 public 方式)。
這意味着,如果你的資料在從資料庫中讀取後需要轉化處理,它可以在每個對象建立時由對象自動處理。
例如,假如每條記錄的位址都需要掩蓋一部分。我們可以在構造函數中操作這個屬性。示例如下:
07 | function __construct($other = '') { |
08 | $this->address = preg_replace('/[a-z]/', 'x', $this->address); |
09 | $this->other_data = $other; |
當資料被擷取到類中時,位址的所有小寫字母 a-z 都被 x 替換。現在,使用類和完成資料轉化是完全透明的。
1 | $STH = $DBH->query('SELECT name, addr, city from folks'); |
2 | $STH->setFetchMode(PDO::FETCH_CLASS, 'secret_person'); |
4 | while($obj = $STH->fetch()) { |
如果位址是 ’5 Rosebud,’,你将看到 ’5 Rxxxxxx’ 這樣的輸出。當然,有時你希望構造函數在資料設定之前被調用。PDO 也考慮到這種情形。
1 | $STH->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'secret_person'); |
現在,當你使用這個 fetch 模式(PDO::FETCH_PROPS_LATE)重複前一個示例代碼時,位址不會被掩蓋,因為構造函數在屬性配置設定之前就被調用了。
最後,如果你真的需要,你可以在使用 PDO 擷取資料到對象中時傳值給構造函數:
1 | $STH->setFetchMode(PDO::FETCH_CLASS, 'secret_person', array('stuff')); |
如果你對每個對象的構造函數傳遞的資料不同,你可以在 fetch 方法中設定 fetch 模式。
2 | while($rowObj = $STH->fetch(PDO::FETCH_CLASS, 'secret_person', array($i))) { |
其它一些有用的方法
盡管并不是說 PDO 就涵蓋了一切(它并非一個龐大的擴充!)。它依然有一些其它的方法,在使用 PDO 做一些基礎工作時會用到。
->lastInsertId() 方法永遠在資料庫句柄上被調用,而非語句句柄,并将傳回該連接配接插入的最後一條插入行的自增長id值。
1 | $DBH->exec('DELETE FROM folks WHERE 1'); |
2 | $DBH->exec("SET time_zone = '-8:00'"); |
->exec() 方法被用來執行那些無傳回值或影響行資料的的指令。上面就是兩條使用exec 方法的例子。
1 | $safe = $DBH->quote($unsafe); |
->quote() 方法将過濾字元串引号,這樣你就可以在查詢語句中安全的使用了。此傳回函數适應于不适用預處理語句的情形。
1 | $rows_affected = $STH->rowCount(); |
->rowCount() 方法傳回一個操作影響資料行的數量整數。在某個已知PDO版本中,根據 [this bug report](http://bugs.php.net/40822) 該方法對 select 語句無效。如果你遭遇此問題,請更新PHP,你也可以使用下面的代碼擷取行數:
01 | $sql = "SELECT COUNT(*) FROM folks"; |
02 | if ($STH = $DBH->query($sql)) { |
04 | if ($STH->fetchColumn() > 0) { |
06 | # issue a real select here, because there's data! |
09 | echo "No rows matched the query."; |