很多程序員都學習過如何使用 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."; |