為什么 PHP 應該使用 PDO 方式訪問數據庫

jopen 11年前發布 | 33K 次閱讀 PDO PHP開發

很多程序員都學習過如何使用 MySQL 或 MySQLi 擴展訪問數據庫。在 PHP 5.1 中,有一個更好的方法。 PHP Data Objects (PDO) 提供了很多預處理語句的方法,且使用對象將使你的工作更有成效!

PDO 介紹

“PDO – PHP Data Objects – 是一個對多種數據庫提供統一操作方法的數據庫訪問層。”

</blockquote>

它并不具備數據庫特有的語法,但它將使切換數據庫和平臺更加容易,多數情況下,只需要簡單修改鏈接字符串。

 為什么 PHP 應該使用 PDO 方式訪問數據庫

這并非一篇完整教導如何使用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 )

你的系統不會也不必支持所有上面的驅動;下面是一個快速檢查所支持數據庫的方法:

print_r(PDO::getAvailableDrivers());  

連接

不同數據庫的連接方法可能稍有不同,下面是一些較為流行的數據庫連接方法。你將注意到,雖然數據庫類型不同,前三種數據庫的連接方式是相同的——而 SQLite 使用自己的語法。

 為什么 PHP 應該使用 PDO 方式訪問數據庫
    try {  
      # MS SQL Server and Sybase with PDO_DBLIB  
      $DBH = new PDO("mssql:host=$host;dbname=$dbname, $user, $pass");  
      $DBH = new PDO("sybase:host=$host;dbname=$dbname, $user, $pass");  

      # MySQL with PDO_MYSQL  
      $DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);  

      # SQLite Database  
      $DBH = new PDO("sqlite:my/database/path/database.db");  
    }  
    catch(PDOException $e) {  
        echo $e->getMessage();  
    }  

注意 try/catch 塊——你應該總是使用 try/catch 包裝你的 PDO 操作,并使用異常機制——這里只是簡單的示例。通常,你只需要一個連接——有很多可以教你語法的列表。 $DBH 代表“數據庫句柄”,這將貫穿全文。

通過將句柄設置為 NULL,你可以關閉任一連接。

# close the connection  
$DBH = null;  

你可以在PHP.net找到更多數據庫特定選項和/或其它數據庫連接字符串的信息。

異常與 PDO

PDO 可以使用異常處理錯誤,這意味著你的所有 PDO 操作都應當包裝在一個 try/catch 塊中。你可以通過設定錯誤模式屬性強制 PDO 在新建的句柄中使用三種錯誤模式中的某一個。下面是語法:

$DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );  
$DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );  
$DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );

無論你設定哪個錯誤模式,一個錯誤的連接總會產生一個異常,因此創建連接應該總是包裝在 try/catch 塊中。

PDO::ERRMODE_SILENT

這是默認的錯誤模式。如果你使用這個模式,你將得使用同 mysql 或 mysqli 擴展一樣的方法差錯。其它兩種模式更適合 DRY 編程。

PDO::ERRMODE_WARNING

此方法將會發出一個標準PHP警告,并允許程序繼續運行。這對調試很有幫助。

PDO::ERRMODE_EXCEPTION

這是多數情況下你所希望的方式。它生成異常,允許你更容易的處理錯誤,隱藏可能導致它人了解你系統的信息。下面是一個充分利用異常的示例:

    # connect to the database  
    try {  
      $DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);  
      $DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );  

      # UH-OH! Typed DELECT instead of SELECT!  
      $DBH->prepare('DELECT name FROM people');  
    }  
    catch(PDOException $e) {  
        echo "I'm sorry, Dave. I'm afraid I can't do that.";  
        file_put_contents('PDOErrors.txt', $e->getMessage(), FILE_APPEND);  
    }  

在 select 語句中有一個故意留下的錯誤;這將導致一個異常。異常錯誤細節保存至一個 log 文件,并生成一段友好的(或不怎么友好的)信息於用戶。

插入和更新

插入新數據,更新已存數據是一種非常常見的數據庫操作。使用 PDO,這通常需要兩個步驟。本節中所述的所有內容對更新插入都有效。

 為什么 PHP 應該使用 PDO 方式訪問數據庫

這里有一個最基本的插入示例:

# STH means "Statement Handle"
$STH = $DBH->prepare("INSERT INTO folks ( first_name ) values ( 'Cathy' )");
$STH->execute();

你也可以使用 exec() 完成相同的操作,這將減少調用。多數情況下,你會使用調用多的方法,以充分利用語句預處理的優勢。即使你只用它一次,使用語句預處理,幫助你保護你的 SQL 免于注入攻擊。

預處理語句

使用語句預處理將幫助你免于SQL注入攻擊。

一條預處理語句是一條預編譯的 SQL 語句,它可以使用多次,每次只需將數據傳至服務器。其額外優勢在于可以對使用占位符的數據進行安全處理,防止SQL注入攻擊。

你通過在 SQL 語句中使用占位符的方法使用預處理語句。下面是三個例子:一個沒有占位符,一個使用無名占位符,一個使用命名占位符。

# no placeholders - ripe for SQL Injection!
$STH = $DBH->("INSERT INTO folks (name, addr, city) values ($name, $addr, $city)");

# unnamed placeholders
$STH = $DBH->("INSERT INTO folks (name, addr, city) values (?, ?, ?);

# named placeholders
$STH = $DBH->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)");

你希望避免第一種方法。選擇命名我無名占位符將會對你對語句中數據的設置產生影響。

無名占位符

# assign variables to each place holder, indexed 1-3
$STH->bindParam(1, $name);
$STH->bindParam(2, $addr);
$STH->bindParam(3, $city);

# insert one row
$name = "Daniel"
$addr = "1 Wicked Way";
$city = "Arlington Heights";
$STH->execute();

# insert another row with different values
$name = "Steve"
$addr = "5 Circle Drive";
$city = "Schaumburg";
$STH->execute();

這里有兩步。首先,我們對各個占位符指定變量(2-4行)。然后,我們對各個占位符指定數據,并執行語句。要發送另一組數據,只需改變這些變量的值并再次執行語句。

這種方法看上去對擁有很多參數的語句很笨拙吧?的確。然而,當數據保存于數組中時,這非常容易簡略:

# the data we want to insert
$data = array('Cathy', '9 Dark and Twisty Road', 'Cardiff');

$STH = $DBH->("INSERT INTO folks (name, addr, city) values (?, ?, ?);
$STH->execute($data);

容易吧!

數組中的數據按順序填入占位符中。 $data[0]是第一個,$data[1]是第二個,依次。不過,要是數組中數據的次序不正確,這將不能正常運行,你需要先對數組排序。

命名占位符

你可能已經開始猜測語法了,不過下面就是示例:

# the first argument is the named placeholder name - notice named
# placeholders always start with a colon.
$STH->bindParam(':name', $name);

你可以看使用快捷方式,但它需使用關聯數組。下面是示例:

# the data we want to insert
$data = array( 'name' => 'Cathy', 'addr' => '9 Dark and Twisty', 'city' => 'Cardiff' );

# the shortcut!
$STH = $DBH->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)");
$STH->execute($data);

數組中的鍵不需要以冒號開頭,但其它部分需要同占位符匹配。如果你有一個二維數組,你只需遍歷它,并對遍歷的每個數組執行語句。

命名占位符的另一個好的功能是直接將對象插入到你的數據庫中,只要屬性同命名字段匹配。下面是一個示例對象,以及如何將它插入到數據庫中的示例:

# a simple object
class person {
    public $name;
    public $addr;
    public $city;

    function __construct($n,$a,$c) {
        $this->name = $n;
        $this->addr = $a;
        $this->city = $c;
    }
    # etc ...
}

$cathy = new person('Cathy','9 Dark and Twisty','Cardiff');

# here's the fun part:
$STH = $DBH->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)");
$STH->execute((array)$cathy);

通過在執行時將對象轉換為數組,輸將將會同數組的鍵一樣對待。

Selecting Data

 為什么 PHP 應該使用 PDO 方式訪問數據庫

數據通過語句句柄的->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 方法,使用如下語法:

$STH->setFetchMode(PDO::FETCH_ASSOC);

你也可以在調用 ->fetch() 方法時直接設定。

FETCH_ASSOC

這個 fetch 創建一個關聯數組,通過列的名稱索引。這對使用過 mysql/mysqli 擴展的人應該相當熟悉。下面是通過此方法獲取數據的示例:

# using the shortcut ->query() method here since there are no variable
# values in the select statement.
$STH = $DBH->query('SELECT name, addr, city from folks');

# setting the fetch mode
$STH->setFetchMode(PDO::FETCH_ASSOC);

while($row = $STH->fetch()) {
    echo $row['name'] . "\n";
    echo $row['addr'] . "\n";
    echo $row['city'] . "\n";
}

while 循環將繼續逐行遍歷結果集,直到遍歷完畢。

FETCH_OBJ

此 fetch 將為返回數據的每一行創建一個標準對象。示例如下:

# creating the statement
$STH = $DBH->query('SELECT name, addr, city from folks');

# setting the fetch mode
$STH->setFetchMode(PDO::FETCH_OBJ);

# showing the results
while($row = $STH->fetch()) {
    echo $row->name . "\n";
    echo $row->addr . "\n";
    echo $row->city . "\n";
}

FETCH_CLASS

對象的屬性將在構造函數被調用之前完成設置,這點非常重要。

此 fetch 方法允許你將獲取結果直接填入你選擇的類中。當使用 FETCH_CLASS 時,對象的屬性將在構造函數被調用之前完成設置。再讀一遍,這點相當哪個重要。如果匹配列名稱的屬性不存在,這些屬相將被創建(以 public 方式)。

這意味著,如果你的數據在從數據庫中讀取后需要轉化處理,它可以在每個對象創建時由對象自動處理。

例如,假如每條記錄的地址都需要掩蓋一部分。我們可以在構造函數中操作這個屬性。示例如下:

class secret_person {
    public $name;
    public $addr;
    public $city;
    public $other_data;

    function __construct($other = '') {
        $this->address = preg_replace('/[a-z]/', 'x', $this->address);
        $this->other_data = $other;
    }
}

當數據被獲取到類中時,地址的所有小寫字母 a-z 都被 x 替換。現在,使用類和完成數據轉化是完全透明的。

$STH = $DBH->query('SELECT name, addr, city from folks');
$STH->setFetchMode(PDO::FETCH_CLASS, 'secret_person');

while($obj = $STH->fetch()) {
    echo $obj->addr;
}

如果地址是 ’5 Rosebud,’,你將看到 ’5 Rxxxxxx’ 這樣的輸出。當然,有時你希望構造函數在數據設置之前被調用。PDO 也考慮到這種情形。

$STH->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'secret_person');  

現在,當你使用這個 fetch 模式(PDO::FETCH_PROPS_LATE)重復前一個示例代碼時,地址不會被掩蓋,因為構造函數在屬性分配之前就被調用了。

最后,如果你真的需要,你可以在使用 PDO 獲取數據到對象中時傳值給構造函數:

$STH->setFetchMode(PDO::FETCH_CLASS, 'secret_person', array('stuff'));

如果你對每個對象的構造函數傳遞的數據不同,你可以在 fetch 方法中設置 fetch 模式。

$i = 0;
while($rowObj =  $STH->fetch(PDO::FETCH_CLASS, 'secret_person', array($i))) {
    // do stuff
    $i++
}

其它一些有用的方法

盡管并不是說 PDO 就涵蓋了一切(它并非一個龐大的擴展!)。它依然有一些其它的方法,在使用 PDO 做一些基礎工作時會用到。

$DBH->lastInsertId();

->lastInsertId() 方法永遠在數據庫句柄上被調用,而非語句句柄,并將返回該連接插入的最后一條插入行的自增長id值。

$DBH->exec('DELETE FROM folks WHERE 1');
$DBH->exec("SET time_zone = '-8:00'");

->exec() 方法被用來執行那些無返回值或影響行數據的的命令。上面就是兩條使用exec 方法的例子。

$safe = $DBH->quote($unsafe);

->quote() 方法將過濾字符串引號,這樣你就可以在查詢語句中安全的使用了。此返回函數適應于不適用預處理語句的情形。

$rows_affected = $STH->rowCount();

->rowCount() 方法返回一個操作影響數據行的數量整數。在某個已知PDO版本中,根據 [this bug report](http://bugs.php.net/40822) 該方法對 select 語句無效。如果你遭遇此問題,請更新PHP,你也可以使用下面的代碼獲取行數:

$sql = "SELECT COUNT(*) FROM folks";
if ($STH = $DBH->query($sql)) {
    # check the row count
    if ($STH->fetchColumn() > 0) {

    # issue a real select here, because there's data!
    }
    else {
        echo "No rows matched the query.";
    }
}

總結

我希望本文能夠幫助你從 mysql 和 mysqli 擴展中遷移出來。你怎么想,有沒有人愿意換個口味嘗個鮮?

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!