一名新 PHP 資料對象 (PDO) 資料抽象層的原始開發人員為您簡要介紹該抽象層,重點講述與 Oracle 一起啟動並執行情況。
需要 PHP:5.0
需要其他:Oracle 8 或更高版本用戶端庫
下載用於 Oracle 的 PDO (Windows):php_pdo.dll, php_pdo_oci.dll
下載用於 Oracle 的 PDO (Unix):pdo, pdo_oci
PDO 簡介
PHP 主要是由志願者完成的項目;儘管有少數一些固定的“核心”開發人員,但是我們沒有一個人在全職受薪的開發 PHP。除此之外,我們分別位於世界不同地方,您可以想象長期開發的協調工作是何等困難。因此,PHP 主要是基於突發奇想的個人短期需求來發展的,其原因也多種多樣,有的是實驗,有的則是因為“明天有活要交”。儘管這樣通常每一步都會改善 PHP,但從長遠來看則是缺乏完整性 - 資料庫擴充就是一個重要的例子。
在各種不同的資料擴充(oci、mysql、postgresql、mssql 等)之間根本沒有真正的一致性,甚至在某些情況下,在這些擴充內部也沒有真正的一致性。幾乎所有這些擴充都在使用與基礎資料庫 API 緊密相連的不同程式碼完成著相同種類的任務。而且因為我們(PHP 核心開發人員和擴充開發人員)的人手非常有限,因此這就造成了代碼更加難以維護,從而為 PHP 帶來了很大的問題。
由於 PHP 越來越受歡迎並不斷成功,因此主要 PHP 資料庫擴充的維護者們參加了在德國舉行的 LinuxTag 2003 大會,在會上我們交換了對 PHP 前景的看法。在討論 PHP 發展的隨機性時,我們確定了在 PHP 中進行資料庫訪問的一些目標:
·提供一種輕型、清晰、方便的 API
·統一各種不同 RDBMS 庫的共有特性,但不排除更進階的特性。
·通過 PHP 指令碼提供可選的較大程度的抽象/相容性。
我們之所以提出了這種 PHP 資料對象 (PDO) 的概念,是因為我們希望通過採用 Zend Engine 2(PHP 5 的核心)先進的物件導向特性獲得該 API 的一些更優秀的效能。
PHP 中的資料抽象層概念一點都算不上新;在 Google 中查詢“PHP database abstraction”會找到大約 83,200 個匹配項。它幾乎是許多 PHP 開發人員夢寐以求的,而其產生則部分歸因於我們不完整的 API。如果您曾經嘗試過使用第三方抽象層來完成任何真正重要的工作,通常會發現這些抽象層對於手頭的工作來說設計的功能過於強大了 - 或者表現為在使用前需要進行大量學習,或者表現為介面速度緩慢,參數需要經過多層指令碼函數調用才能到達資料庫自有的 API;通常是存在上述兩種表象。
為什麼這些抽象層會存在這種問題?這些抽象層總是在試圖完成太多的任務,甚至可能是不可能的任務。我們決定以實用為目標,僅將一些最常見的資料庫 API 特性作為我們的基礎,並使得 PDO 驅動程式能夠將它們特定於產品的特性暴露為常規擴充函數。
為什麼使用 PDO?
聽過有關資料庫抽象擴充謠傳的大多數人會立刻對 PDO 的擴充方面產生疑惑 - 我們是否要分析 SQL,將其轉換為相應的後端方言呢?我們如何處理特性 X 或特性 Y,等等。因此,當您聽說我們在 PDO 中根本不用為此而擔憂時可能會大吃一驚;我們不希望使所有內容都完全統一,因為要使得這種統一成為可能,只能是將自己限制在最低的通用標準。
如果 PDO 不是一個整體的抽象層,那還有什麼別的原因值得您考慮使用它嗎?
·效能。PDO 從一開始就吸取了現有資料庫擴充成功和失敗的經驗教訓。因為 PDO 的代碼是全新的,所以我們有機會重新開始設計效能,以利用 PHP 5 的最新特性。
·能力。PDO 旨在將常見的資料庫功能作為基礎提供,同時提供對於 RDBMS 獨特功能的方便訪問。
·簡單。PDO 旨在使您能夠便於使用資料庫。API 不會強行介入您的代碼,同時會清楚地表明每個函數調用的過程。
·運行時可擴充。PDO 擴充是模組化的,使您能夠在運行時為您的資料庫後端載入驅動程式,而不必重新編譯或重新安裝整個 PHP 程式。例如,PDO_OCI 擴充會替代 PDO 擴充實現 Oracle 資料庫 API。還有一些用於 MySQL、PostgreSQL、ODBC 和 Firebird 的驅動程式,更多的驅動程式尚在開發。
您可能想瞭解 PDO 與其他常用的抽象層的對比情況,例如 PEAR DB 或 ADODB。無論在 API 方面還是在效能方面,PDO 都比其他常見抽象層要輕型,但是涉及到在各個資料庫後端之間提供統一性方面,則不如那些抽象層,例如用於處理大量可移植性問題的 PEAR MDB 2 抽象層。
在哪裡可以獲得 PDO?
PDO 是通過 PECL(發音為“pee-kle”,歐洲語言風格),即 PHP 擴充庫提供的。如果您在運行 Linux 電腦,請按照下面的說明進行設定;稍後是在 Windows 上安裝的詳細資料。
請注意,PDO 及其驅動程式當前處於“alpha”狀態;這就意味著我們會合理保證沒有重大缺陷,但是該程式包功能並不完善 - 我們還要添加很多功能。雖然我們鼓勵您測試該程式包,但是實在不推薦在現階段將其用於生產。
Unix/Linux 安裝
如果您以前尚未嘗試過 PHP 5,則請花一點時間來通讀一下“新聞”和各種聲明。在 UNIX 電腦上,您可能要安裝或升級 libxml2;如果沒有 libxml2,“pear”程式包管理工具就無法運行,您安裝 PDO 時就會遇到很多困難。擷取 PHP 5,並將其編譯和安裝。確保指定的首碼不是 /usr/local/,這樣它就不會與 PHP 4 安裝發生衝突了:
% ./configure --prefix=/usr/local/php5 --with-zlib [此處指定其他選項]
% make install
現在您就可以使用“pear”工具擷取並安裝 PDO 以及用於 PDO 的 Oracle 驅動程式了。因為 PDO 當前標記為 alpha,所以預設情況下 pear 工具不會下載該程式包。在該程式包名稱後面添加尾碼“-alpha”,通知該 pear 工具可以安裝 alpha 版本:
% PATH="/usr/local/php5/bin:$PATH"
% pear install PDO-alpha
您需要告知 PHP 從專用於 PHP 5 的 php.ini 檔案載入 PDO 驅動程式。如果您使用的首碼與我使用的一樣,PHP 則會在 /usr/local/php5/lib/php.ini 中尋找 php.ini 檔案。向該檔案中添加以下行:
extension=pdo.so
現在您需要擷取資料庫特定的驅動程式;對於 Oracle,此特定程式稱為 PDO_OCI。在 shell 中,鍵入:
% pear install PDO_OCI-alpha
此驅動程式也需要從 php.ini 檔案載入;將下行添加到前面添加的那行之後:
extension=pdo_oci.so
現在檢查一下,確保它能夠運行:
% php -m
在模組列表中,您應該會看到 PDO 和 PDO_OCI。
防火牆礙事了?
如果您位於防火牆的後面,則在使用 pear 安裝程式擷取程式包時可能會遇到一些問題。如果發生這種情況,則可以按照下列說明手動下載並安裝這些程式包:
% wget http://pecl.php.net/get/PDO
% pear install PDO-0.1.1.tgz
[ 將 extension=pdo.so 添加到 php.ini ]
% wget http://pecl.php.net/get/PDO_OCI
% pear install PDO_OCI-0.1.tgz
[ 將 extension=pdo_oci.so 添加到 php.ini ]
在上述兩種情況下,都需要首先調用“pear install”(後跟下載的真正程式包);上述樣本中的版本號碼在本文編寫之時是最新的,但隨著開發的繼續進行會發生變化。
Windows 安裝
如果您正在運行 Windows,則請按照下列說明執行:
·從 http://www.php.net/downloads.php#v5 擷取 PHP 5,將其解壓縮到 C:\php5。
·從 http://snaps.php.net/win32/PECL_5_0/php_pdo.dll 和http://snaps.php.net/win32/PECL_5_0/php_pdo_oci.dll 分別擷取 PDO 和 PDO_OCI,將其放入 C:\php5\ext。或者,您可以從 PHP 5 下載頁上列出的“用於 PHP 5.0.0 的 PECL 模組集合”zip 檔案中找到所有這些 PDO 驅動程式,以及所有 PECL 程式包的所有 Windows 版本。
·編輯 C:\php5\php.ini 檔案,並添加下列內容:
extension=php_pdo.dll
extension=php_pdo_oci.dll
編輯 php.ini 檔案時,有一點很重要,即要在任何其他 PDO 驅動程式之前先載入 PDO 擴充,否則就不能正確初始化(在這種情況下會出錯)。
如果在 Windows 目錄中有一個 PHP 4 的全域 php.ini 檔案,則可能會遇到問題。最好的解決方案是,移動該 php.ini 檔案,使其與 PHP 4 SAPI 位於相同的檔案夾中,以隔離 PHP 4 安裝;例如,將其移動到與 php4apache.dll 相同的檔案夾中。請注意,PHP 5 程式中並非所有文檔都是最新的;推薦的安裝過程如上面所述 - 如 install.txt 檔案所聲明的,請勿將任何 DLL 複製到 windows 檔案夾或 system 檔案夾中 - 任何內容都是自包含的。如果您啟動並執行是 apache,並且遇到無法載入 DLL 的錯誤,則檢查一下是否將 C:\php5 添加到了 PATH 中。另外,還要注意 PHP 5 的 CGI 版本現在的名稱為 php-cgi.exe。
串連 PDO
首先建立 PDO 類的一個執行個體,將其用作資料庫控制代碼。使用哪個基礎驅動程式並不重要;您總要使用 PDO 類名。建構函式的第一個參數為資料來源名稱 (DSN),第二個參數為使用者名稱,第三個參數為該使用者名稱的口令。DSN 的 PDO 命名慣例為 PDO 驅動程式的名稱,後面一個冒號,再後面是可選的驅動程式特定的資訊。在我們的樣本中,會載入 OCI 驅動程式但不指定任何其他資訊;這樣會使用預設的資料庫。對於其他驅動程式,如 ODBC 驅動程式,第一個冒號後面的所有內容都將被用作 ODBC DSN。MySQL 驅動程式會同樣以不同的方式解釋它的 DSN。
如果無法載入該驅動程式,或者發生了串連失敗,則會拋出一個 PDOException,以便您可以決定如何最好地處理該故障。
<?php
try {
$dbh = new PDO("OCI:", "scott", "tiger");
} catch (PDOException $e) {
echo "Failed to obtain database handle " .$e->getMessage();
}
?>
在連接字串中,您可以指定兩個選擇性參數;第一個是資料庫名稱,第二個是字元集;這些參數與可選的第三個和第四個參數相對應,後兩個參數您可能在 oci8 擴充函數 ociconnect() 或 ociplogon() 中使用過。要使用特定的字元集串連一個特定的資料庫,則可以執行下列操作:
<?php
try {
$dbh = new PDO("OCI:dbname=accounts;charset=UTF-8", "scott", "tiger");
} catch (PDOException $e) {
echo "Failed to obtain database handle " .$e->getMessage();
}
?>
省略 try..catch 控制結構並無裨益。如果在應用程式的較進階別沒有定義異常處理,則在無法建立資料庫連接的情況下,該指令碼會終止。
串連管理
目前,PDO 完全沒有執行自己的任何串連管理,因此每個“新 PDO”調用都會建立一個新的資料庫連接。該串連在 $dbh 變數越界時,或者當您為其指定 NULL 值時會被釋放。
<?php
try {
$dbh = new PDO("OCI:dbname=accounts;charset=UTF-8", "scott", "tiger");
} catch (PDOException $e) {
echo "Failed to obtain database handle " .$e->getMessage();
exit;
}
// 在此處對資料庫執行一些操作
// ...
// 現在完成,釋放該串連
$dbh = null;
?>
計劃在不久的將來為 PDO 增加串連緩衝功能;就當前的 oci8 擴充而言,會重用與現有伺服器的串連,並且在這些串連中,還會重用閑置的登入。當在緩衝串連模式中運行時,如上面的程式碼片段所示釋放 $dbh 時會將該登入標記為可由其他串連重用。
如果您使用 ODBC 驅動程式訪問 Oracle,則可能會很高興地注意到,預設情況下 PDO_ODBC 驅動程式支援 ODBC 串連池。
使用 PDO
瞭解一個編程 API 的最好方式就是使用它,因此我們來看一下附帶的這個示範,以瞭解如何進行批次更新(代碼如下)。
<?php
// Create a PDO database handle object
// the 'oci:' string specifies that the OCI driver should be used
// you could use 'oci:dbname=name' to specify the database name.
// The second and third parameters are the username and password respectively
$dbh = new PDO('oci:', 'scott', 'tiger');
// Create a test table to hold the data from credits.csv
$dbh->exec("
CREATE TABLE CREDITS (
extension varchar(255),
name varchar(255)
)");
// start a transaction
$dbh->beginTransaction();
// prepare to insert a large quantitiy of data
$stmt = $dbh->prepare("INSERT INTO CREDITS (extension, name) VALUES (:extension, :name)");
// bind the inputs to php variables; specify that the data will be strings
// with a maximum length of 64 characters
$stmt->bindParam(':extension', $extension, PDO_PARAM_STR, 64);
$stmt->bindParam(':name', $name, PDO_PARAM_STR, 64);
// Open the .csv file for import
$fp = fopen('credits.csv', 'r');
while (!feof($fp)) {
list($extension, $name) = fgetcsv($fp, 1024);
$stmt->execute();
}
fclose($fp);
// Commit the changes
$dbh->commit();
?>
既然我們已經成功串連到了 Oracle,那麼現在就可以建立一個表來儲存一些資料了。對於此樣本,我們使用一些 PHP 擴充及其作者,並將這些內容輸入一個資料庫中。資料庫控制代碼對象的 exec() 方法可用來發出不會返回結果集的快速一次性查詢,因此我們在這裡使用該方法來發出 CREATE TABLE 查詢。
為了使得樣本更自然,我從 PHP 原始碼中抽取了擴充及其作者的資訊,並將其儲存到了一個 CSV 檔案中(請參見“相關附件:credits.csv”)。這就代表一個常見情形:從 CSV 檔案批次匯入資料。在我們的樣本中,我們充分利用了 Oracle 的預先處理語句和綁定參數,以獲得一個高效的資料匯入指令碼。在講述該樣本之前,有必要瞭解一下 PDO 處理事務的方式。
PDO 中的交易處理
Oracle 具有一個敏感的預設操作模式:當您進行串連時,將會位於一個隱含交易處理中,在提交事務之前其中的更改不會完全生效。除了交易處理的標準優點(原子性、一致性、隔離性、可持久性 - ACID)之外,資料庫伺服器在執行每次更新之後還不需要重新構建索引和其他內部結構;它可以延遲到提交之後進行。這樣會加速代碼的執行。Oracle 這點確實很好。
但不幸的是,並非每個資料庫供應商都支援交易處理,並且因為 PDO 旨在以一種相對可移植的方式支援這些交易處理,所以它預設情況下以自動認可模式運行。啟用自動認可模式後,資料庫驅動程式會隱式提交每個成功的更新。當您調用 $dbh->beginTransaction() 時,就會請求關閉自動認可,直到調用 $dbh->commit() 或者 $dbh->rollBack() 才會重新啟用,具體取決於您的代碼是怎樣編寫的。如果基礎驅動程式不支援交易處理,則會拋出一個 PDOException。
如果發生了問題並且 PHP 出錯,您的指令碼將退出並且事務處於待批狀態;或者您關閉資料庫控制代碼時,PDO 會自動針對任何待批的事務調用 $dbh->rollBack()。此行為會減少向資料庫中提交可能未定義或者已損壞資料的可能性,這是用於處理已放棄事務的標準語義。
預先處理語句、預存程序
PDO 支援使用 Oracle 樣式命名的預留位置文法將變數幫定到 SQL 中的預先處理語句(與 oci8 擴充中的 ocibindbyname() 類似)。PDO 還為其他資料庫(如 ODBC)提供了命名預留位置類比,甚至可以為生來就不支援該概念的資料庫(如 MySQL)類比預先處理語句和綁定參數。這是 PHP 向前邁進的積極一步,因為這樣可以使開發人員能夠用 PHP 編寫“企業級”的資料庫應用程式,而不必特別關注資料庫平台的能力。
使用 PDO 預先處理語句非常簡單,調用資料庫控制代碼的 prepare() 方法即可。它會返回一個語句控制代碼對象,然後您可以使用該對象來綁定參數和執行語句。在此樣本中,我們將要定義兩個命名預留位置,“:extension”和“:name”,這兩個預留位置分別與 .CSV 檔案中的 PHP 副檔名稱和其中一個作者的姓名相對應。
$stmt = $dbh->prepare("INSERT INTO CREDITS (extension, name) VALUES (:extension, :name)");
預先處理了語句之後,我們使用 bindParam() 方法來將這些具名引數分別與 PHP 變數名稱“$extension”和“$name”相關聯(這與 ocibindbyname() 類似)。我們還會通知 Oracle,這些資料將要格式化為字串,最大長度為 64 個字元。
$stmt->bindParam(':extension', $extension, PDO_PARAM_STR, 64);
$stmt->bindParam(':name', $name, PDO_PARAM_STR, 64);
我們現在即準備好插入資料了 - 我們只需要開啟該 CSV 檔案,並從中擷取資料即可。通過使用 fopen() 和 fgetcsv() 函數可以相當簡單地完成此操作。然後,我們可以使用 PHP list() 建構函式直接將 CSV 的列指定給變數“$extension”和“$name”。因為這些變數已經綁定到了語句中,所以我們現在要做的只是調用該語句對象的 execute() 方法使其執行插入。這種方式既方便又快捷 - 在交易處理時每個迭代迴圈只有兩行。到達檔案尾時,我們就可以立即使用資料庫控制代碼的 commit() 方法來提交這些更改了。
如果您只是要傳遞輸入參數,並且有許多這樣的參數要傳遞,那麼您會覺得下面所示的捷徑文法非常有協助;此文法使您能夠省去對 $stmt->bindParam() 的調用。
$stmt = $dbh->prepare("INSERT INTO CREDITS (extension, name) VALUES (:extension, :name)");
$stmt->execute(array(':extension' => $extension, ':name' => $name));
您還可以使用 bindParam 來為預存程序設定輸入/輸出參數;文法是完全相同的,只是查詢有所不同。下面的代碼示範如何調用一個名為“sp_add_item”的預存程序;其目的是要針對輸入設定 $item_name,然後該預存程序將在返回時更新 $error_code。
$stmt = $dbh->prepare("begin sp_add_item(:item_name, :error_code); end");
$stmt->bindParam(':item_name', $item_name, PDO_PARAM_STR, 12);
$stmt->bindParam(':error_code', $error_code, PDO_PARAM_STR, 12);
$stmt->execute();
抓取資料
使用 PDO 抓取資料與進行插入或更新相似,只是您執行完查詢之後,將要重複調用 fetch() 方法來擷取結果集的下一行。進行擷取的最簡單情況如下所示,值得注意的一點是,您還可以將參數綁定到查詢,以控制如 WHERE 子句這樣的內容;執行此操作的文法與我們已經看到的 bindParam() 代碼完全相同。
$stmt = $dbh->prepare("SELECT extension, name from CREDITS");
if ($stmt->execute()) {
while ($row = stmt->fetch()) {
print_r($row);
}
}
PDO 支援一些不同的抓取策略,這些策略在方便性和效能方面有所差別;通過將下列選項之一指定為 fetch() 方法的參數,您可以更改其傳回值以適應您的文法:
·PDO_FETCH_NUM - 每個行抓取返回一個按照列位置索引的數組,並且以 0 為基數(第一列是第 0 個元素)。
while ($row = $stmt->fetch(PDO_FETCH_NUM)) {
printf("Extension %s, by %s<br>", $row[0], $row[1]);
}
·PDO_FETCH_ASSOC - 每個行抓取根據行集中的列名,返回一個按列名索引的數組。
while ($row = $stmt->fetch(PDO_FETCH_ASSOC)) {
echo "Extension $row[EXTENSION] by $row[NAME]<br>";
}
·PDO_FETCH_BOTH - 每個行抓取返回一個既按照列位置又按照列名索引的數組。也就是上述兩種情況的直接組合。如果沒有指定抓模數式,則該模式為預設模式。
·PDO_FETCH_OBJ - 每個行抓取返回一個匿名對象,其屬性名稱與列名對應。
while ($row = $stmt->fetch(PDO_FETCH_ASSOC)) {
echo "Extension {$row->EXTENSION} by {$row->NAME}<br>";
}
·PDO_FETCH_LAZY - 每個行抓取返回一個引用語句對象的重載對象。這“看起來”好像是 PDO_FETCH_OBJ 和 PDO_FETCH_BOTH 的組合,只是只有當您在指令碼中訪問 PHP 變數時才建立這些變數。
·PDO_FETCH_BOUND - 抓取每行,返回 TRUE。在使用綁定輸出資料行時這種方式非常有用,它可以避免建立不需要的任何數組或對象。(請參見下面的樣本)。
無論您使用哪種抓取策略,當沒有其他行可抓取時,fetch() 方法將會返回 FALSE。
現在我要講述一些技巧,如果您需要最後再調整一下指令碼效能的話,這些技巧可能會對您有所協助。但先給你一個忠告:要像躲避瘟疫一樣避免不成熟的最佳化。您應該總是首選最清晰、可維護性最好的解決方案。請記住,在一個典型的 Web 應用程式中,您不能衡量各種抓模數式間的區別,除非指令碼要處理很多行。我再重複一遍:抓模數式間的效能區別非常小 - 請使用最適合您代碼的模式。
請記住,使用 PDO_FETCH_NUM 的花銷最小,因為訪問列資料只是一個簡單的數值查詢。PDO_FETCH_OBJ 使您能夠使用 OO 文法將資料集的列作為對象的屬性來訪問,但是每個屬性訪問都涉及一個附加的散列查詢,使得使用它的花銷基本上與 PDO_FETCH_ASSOC 相同。每個這樣的模式都會複製整行,從而佔用稍多的記憶體。
很多資料庫驅動程式都會代表您預先抓取並緩衝一定數量的行。PHP 每次訪問其中一個這樣行中的列時,它都需要將其複製到自己的專用記憶體地區中。如果您的查詢涉及很多行,而只需要基於某種複雜的邏輯訪問給定行的特定列,則您會發現 PDO_FETCH_LAZY 是一種避免使用很多記憶體的有用方法,因為它只有在您訪問給定列時才複製該列。使用此方式時要注意,從某個給定語句為每個 fetch() 抓取的“惰性對象”是每次迭代時使用的同一對象(以減少每次建立/銷毀它的開銷)。這就暗示著您不能只是簡單地儲存該對象用於以後的比較,因為它仍然會引用該語句的當前行 - 您需要手動複製所需要的部分。
最後一種模式為 PDO_FETCH_BOUND,該模式會告知 PDO 您已經將所有資料行繫結到了 PHP 變數,並且除了要它在到達行集的末尾時通知您外不需要它執行別的任何操作。綁定輸出資料行在概念上與綁定輸入參數相似,只是綁定輸出資料行可以用於所有資料庫驅動程式。您可以將 PHP 變數綁定到命名列,PDO 將在每次調用 execute() 時對其進行更新。此技術可用來剃去結果集中每列、每行的一些虛擬機器作業碼(這種代碼速度比原生碼要慢)。這種技術的缺點在於,可能會使您的代碼難以跟蹤(也稱為 WTF 係數較高),您使用變數名稱時需要倍加小心。下面的代碼說明了綁定輸出資料行的使用。請注意,您不必指定 PDO_FETCH_BOUND 即可使用 $stmt->bindColumn();PDO_FETCH_BOUND 只是一個對於您瞭解只能使用綁定值的情況的一種最佳化。
$stmt = $dbh->prepare("SELECT extension, name from CREDITS");
if ($stmt->execute()) {
$stmt->bindColumn('EXTENSION', $extension);
$stmt->bindColumn('NAME', $name);
while ($stmt->fetch(PDO_FETCH_BOUND)) {
echo "Extension:$extension, Author:$name\n";
}
}
可移植性
區分大小寫列
PDO 旨在令使用可移植 SQL 的指令碼運行良好、可移植。本文中提及的所有查詢(調用預存程序除外)在使用任何 PDO 驅動程式時其運行效能應該相同 - 包括所有綁定輸入變數和綁定輸出資料行。
但有一個轉換問題 - 當您使用 PDO_FETCH_ASSOC 抓取資料時,不同的驅動程式會以不同的方式返回列名 - 某些會將列名轉化為大寫,某些轉換為小寫,某些則會使其呈查詢中指定的樣式。這對於 PHP 指令碼來說是一個潛在的問題,因為數組鍵區分大小寫。PDO 提供了一個相容性屬性來協助規範指令碼的結果。下面的小程式碼片段是上面 PDO_FETCH_BOUND 樣本的可移植版本,因為 setAttribute() 方法調用會指導 PDO 將抓取返回的列名全部轉換為大寫:
$dbh = new PDO('OCI:', 'scott', 'tiger');
$dbh->setAttribute(PDO_ATTR_CASE, PDO_CASE_UPPER);
stmt = $dbh->prepare("SELECT extension, name from CREDITS");
if ($stmt->execute()) {
$stmt->bindColumn('EXTENSION', $extension);
$stmt->bindColumn('NAME', $name);
while ($stmt->fetch(PDO_FETCH_BOUND)) {
echo "Extension:$extension, Author:$name\n";
}
}
除了 PDO_CASE_UPPER 之外,還有 PDO_CASE_LOWER(它會將列名轉換為小寫)和 PDO_CASE_NATURAL(它是預設選項:使列保持資料庫驅動程式返回的形式)。
錯誤和錯誤處理
可移植指令碼的另一個難題是處理從各種資料庫處理常式返回的各種不同的錯誤訊息;某些資料庫對於程式化處理錯誤的支援能力很差,而其他一些資料庫則具有非常豐富的錯誤碼。只要可行,PDO 將為您的指令碼提供一個統一的錯誤碼,從而使您不必為應對可移植性的這個方面所累。當然,PDO 還會為驅動程式提供原生錯誤碼和錯誤訊息,以防您需要用它來進行診斷,或者錯誤碼映射不完整。
另一個困擾 PHP 資料庫擴充的一致性問題是錯誤處理策略的一致性:某些擴充會返回的錯誤碼需要您手動抓取錯誤字串,而其他一些擴充則只是發出 PHP 警告。PDO 允許您從下列三種不同的錯誤處理策略中選擇一種:
·PDO_ERRMODE_SILENT
這是預設模式;它只是使用語句和資料庫控制代碼對象的 errorCode() 和 errorInfo() 方法為您設定要檢查的錯誤碼。
if (!$dbh->exec($sql)) {
echo $dbh->errorCode() ."<BR>";
$info = $dbh->errorInfo();
// $info[0] == $dbh->errorCode() 統一的錯誤碼
// $info[1] 是驅動程式特定的錯誤碼
// $info[2] 是驅動程式特定的錯誤字串
}
·
PDO_ERRMODE_WARNING
除了設定錯誤碼之外,PDO 還會發出 PHP 警告,您可以使用常規的 PHP 錯誤處理程式捕獲該警告,並集中應用您準備好用於應用程式的任何錯誤處理/記錄策略,或者只是使該錯誤顯示在瀏覽器中(在自我裝載過程中非常有用)。
·
PDO_ERRMODE_EXCEPTION
除了設定錯誤碼之外,PDO 還會拋出一個 PDOException,並將其屬性設定為包含該錯誤碼和資訊。然後,您可以在代碼的較進階別捕獲該異常,使用全域例外處理常式捕獲該異常,或者不對其進行處理而終止指令碼(此時將復原任何未決的事務)。
try {
$dbh->exec($sql);
} catch (PDOException $e) {
// 顯示警告訊息
print $e->getMessage();
$info = $e->errorInfo;
// $info[0] == $e->code; unified error code
// $info[1] 是驅動程式特定的錯誤碼
// $info[2] 是驅動程式特定的錯誤字串
}
請注意,與警告或異常相比,靜默模式針對執行階段錯誤使用的資源最少,但是為了獲得該速度,您犧牲了一些簡單性,而變得有一點複雜。
統一錯誤碼表當前包括下列常量: PDO_ERR_NONE、PDO_ERR_CANT_MAP、PDO_ERR_SYNTAX、PDO_ERR_CONSTRAINT、PDO_ERR_NOT_FOUND、PDO_ERR_ALREADY_EXISTS、PDO_ERR_NOT_IMPLEMENTED、PDO_ERR_MISMATCH、PDO_ERR_TRUNCATED、PDO_ERR_DISCONNECTED。
這些常量所代表的意思字面即可推知,但是 PDO_ERR_CANT_MAP 代碼除外;這是一個 PDO 特定的代碼,也就是說它無法將驅動程式特定的代碼映射到統一的錯誤碼,因此您應該查詢 errorInfo() 方法返回的驅動程式特定代碼來獲得更多資訊。
資料類型
PDO 在某種程度上類型不可知,因此它喜歡將資料表示為字串,而不是將其轉換為整數或雙精確度類型。此時您可能對此有些迷惑,但是原因非常簡單:字串類型是最精確的類型,在 PHP 中具有最廣泛的應用範圍;過早地將資料轉換為整數或者雙精確度類型可能會導致截斷或舍入錯誤。通過將資料以字串抽出,PDO 為您提供了一些指令碼控制,您可以使用普通的 PHP 類型轉換工具(如數學運算過程中的轉換和隱式)來控制如何進行轉換以及何時進行轉換。
NULL
如果結果集中的某列包含一個 NULL 值,PDO 則會將其映射為 PHP null 值。Oracle 在將資料返回 PDO 時會將Null 字元串轉換為 NULL,但是 PHP 支援的任何其他資料庫都不會這樣處理,從而導致了可移植性問題。PDO 提供了一個驅動程式級屬性 PDO_ATTR_ORACLE_NULLS,該屬性會為其他資料驅動程式類比此行為:
$dbh = new PDO('OCI:', 'scott', 'tiger');
$dbh->setAttribute(PDO_ATTR_ORACLE_NULLS, true);
// 現在從此 $dbh 開啟的任何語句中的
// Null 字元串都將被轉換為 NULL
POD 的現狀和未來
PDO 現在仍相當不成熟,但是會快速成熟起來。在編寫本文之時,我在本文中提到的任何內容都能夠通過 PDO_OCI 驅動程式適用於 Oracle 8 或更高版本(在 Oracle 8.0 和 9.2 上測試過)。
已經計劃增加以下主要特性,在不久將可以使用:
1.使用 PHP 流的 LOB 支援。 使用綁定參數,您能夠將任何流資源(如檔案、通訊端、HTTP 資源、壓縮/篩選的流)作為輸入或輸出參數傳遞到在 LOB 上啟動並執行查詢中。與之相似,類型為 LOB 的輸出參數將表現為 PHP 流,因此您可以使用 fread()、fwrite()、fseek() 和其他流函數來訪問這些參數。此時,在 PDO 中根本沒有 LOB 支援。
2.持久性串連和緩衝的預先處理語句。 持久性串連使您能夠避免在每個頁面命中時開啟和關閉資料庫伺服器串連。緩衝的預先處理語句又前進了一步,它使您能夠持久保持查詢的預先處理版本以及資料庫控制代碼。
3.遊標。 目前,PDO 只提供前向唯讀遊標,但是將來會提供可滾動遊標(需要基礎驅動程式支援)、REF-CURSOR、使用遊標進行定點更新,以及可更新滾動遊標。
我們希望在 PHP 5.1 中預設啟用 PHP 擴充(距此目標尚遠),但是在此之前,我們希望能讓 PDO 在 PHP 5.0 發布時穩定運行,但是我們日常工作中的壓力稍稍拖延了這些工作。同時,通過 PECL 發布 PDO 使我們能夠在收到問題報告時做出回應,並根據不同於 PHP 5.0 發布時間表的時間表發布修複版本,因此您在 PHP 5.1 發布前即可使用 PDO。
我們需要您的反饋
如果您試用了 PDO,並且發現了問題,請務必使用我們的錯誤跟蹤軟體將其報告給我們。如果您使用的是 Oracle 驅動程式,則請使用此頁:
http://pecl.php.net/bugs/report.php?package=PDO_OCI
如果您使用的是其他驅動程式,則請用其名稱替換該 URL 中 PDO_OCI。
如果您使用 PDO 時遇到問題,或者針對某些特性存在疑問,或者具有特性請求,請聯絡 pecl-dev@lists.php.net。如果您願意,當然還可以直接聯絡我 (wez@php.net),但是請注意,我每天都會收到大量有關 PHP 的電子郵件;您可能會發現如果首先與前面的郵件清單聯絡會更快得到回覆。
-----------
關於作者
Wez Furlong 是 Brain Room Ltd. 的技術總監,他在該公司不但使用 PHP 用於 Web 開發,還將其用作 Linux 和 Windows 應用程式和系統的嵌入式指令碼引擎。Wez 是 PHP 的核心開發人員,經常向 SQLite、COM/.Net、ActivePHP、mailparse 和 Streams API 等投稿,他是 PECL 即 PHP 擴充社區庫的“頭兒”。他的諮詢公司的網頁為 http://www.thebrainroom.net。