
日期:2006-09-21 作者:喜騰小二 來源:PHPChina
一名新 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 中進行資料庫訪問的一些目的:
我們之所以提出了這種 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 與其他常用的抽象層的對比情況,例如 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 [此處指定其他選項]現在您就可以使用“pear”工俱獲取並安裝 PDO 以及用於 PDO 的 Oracle 驅動程式了。因為 PDO 當前標記為 alpha,所以預設情況下 pear 工俱不會下載該套裝程式。在該套裝程式名稱後麵新增尾碼“-alpha”,通知該 pear 工俱可以安裝 alpha 版本:
% PATH="/usr/local/php5/bin:$PATH"您需要告知 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”(後跟下載的真正套裝程式);上述範例中的版本號在本文編寫之時是最新的,但隨着開發的繼續進行會發生變化。
Windows 安裝
如果您正在執行 Windows,則請按照下列幫助執行:
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,以便您可以決定如何最好地處理該故障。
try {在連線字串中,您可以指定兩個可選參數;第一個是資料庫名稱,第二個是字元集;這些參數與可選的第三個和第四個參數相對應,後兩個參數您可能在 oci8 延伸函式 ociconnect() 或 ociplogon() 中使用過。要使用特定的字元集連線一個特定的資料庫,則可以執行下列操作:
try {省略 try..catch 控制結構並無裨益。如果在應用程式的較進階別沒有定義異常處理,則在無法建立資料庫連線的情況下,該指令檔會終止。
連線管理
目前,PDO 完全沒有執行自己的任何連線管理,因此每個“新 PDO”調用都會建立一個新的資料庫連線。該連線在 $dbh 變數溢位時,或者當您為其指定 NULL 值時會被釋放。
try {計劃在不久的將來為 PDO 增加連線快取功能;就當前的 oci8 延伸而言,會重用與現有伺服器的連線,並且在這些連線中,還會重用閒置的登入。當在快取連線模式中執行時,如上麵的程式碼段所示釋放 $dbh 時會將該登入標記為可由其他連線重用。
如果您使用 ODBC 驅動程式訪問 Oracle,則可能會很高興地注意到,預設情況下 PDO_ODBC 驅動程式支援 ODBC 連線池。
使用 PDO
瞭解一個編程 API 的最好方式就是使用它,因此我們來看一下隨附的這個演示,以瞭解如何進行批次更新(請參見示常式式碼)。
既然我們已經成功連線到了 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);我們現在即準備好插入資料了 - 我們隻需要開啟該 CSV 檔案,並從中獲取資料即可。透過使用 fopen() 和 fgetcsv() 函式可以相當簡單地完成此操作。然後,我們可以使用 PHP list() 構造函式直接將 CSV 的列指定給變數“$extension”和“$name”。因為這些變數已經繫結到了陳述式中,所以我們現在要做的隻是調用該陳述式物件的 execute() 方法使其執行插入。這種方式既方便又快捷 - 在事務處理時每個叠代迴圈隻有兩行。到達檔案尾時,我們就可以立即使用資料庫句柄的 commit() 方法來提交這些變更了。
如果您隻是要傳遞輸入參數,並且有許多這樣的參數要傳遞,那麼您會覺得下麵所示的捷徑語法非常有說明;此語法使您能夠省去對 $stmt->bindParam() 的調用。
$stmt = $dbh->prepare("INSERT INTO CREDITS (extension, name) VALUES (:extension, :name)");您還可以使用 bindParam 來為存儲過程設定輸入/輸出參數;語法是完全相同的,隻是查詢有所不同。下麵的程式碼演示如何調用一個名為“sp_add_item”的存儲過程;其目的是要針對輸入設定 $item_name,然後該存儲過程將在返回時更新 $error_code。
$stmt = $dbh->prepare("begin sp_add_item(:item_name, :error_code); end");抓取資料
使用 PDO 抓取資料與進行插入或更新相似,隻是您執行完查詢之後,將要重複調用 fetch() 方法來獲取結果集的下一行。進行獲取的最簡單情況如下所示,值得注意的一點是,您還可以將參數繫結到查詢,以控制如 WHERE 子句這樣的內容;執行此操作的語法與我們已經看到的 bindParam() 程式碼完全相同。
$stmt = $dbh->prepare("SELECT extension, name from CREDITS");PDO 支援一些不同的抓取策略,這些策略在方便性和效能方麵有所差別;透過將下列選項之一指定為 fetch() 方法的參數,您可以變更其返回值以適應您的語法:
現在我要講述一些技巧,如果您需要最後再調整一下指令檔效能的話,這些技巧可能會對您有所說明。但先給妳一個忠告:要像躲避瘟疫一樣避免不成熟的最佳化。您應該總是首選最清晰、可維護性最好的解決方案。請記住,在一個典型的 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");可移植性
大小寫視為相異的列
PDO 旨在令使用可移植 SQL 的指令檔執行良好、可移植。本文中提及的所有查詢(調用存儲過程除外)在使用任何 PDO 驅動程式時其執行效能應該相同 - 包括所有繫結輸入變數和繫結輸出列。
但有一個轉換問題 - 當您使用 PDO_FETCH_ASSOC 抓取資料時,不同的驅動程式會以不同的方式返回列名 - 某些會將列名轉化為大寫,某些轉換為小寫,某些則會使其呈查詢中指定的樣式。這對於 PHP 指令檔來說是一個潛在的問題,因為陣列鍵大小寫視為相異。PDO 提供了一個相容性內容來說明規範指令檔的結果。下麵的小程式碼段是上麵 PDO_FETCH_BOUND 範例的可移植版本,因為 setAttribute() 方法調用會指導 PDO 將抓取返回的列名全部轉換為大寫:
$dbh = new PDO('OCI:', 'scott', 'tiger');除了 PDO_CASE_UPPER 之外,還有 PDO_CASE_LOWER(它會將列名轉換為小寫)和 PDO_CASE_NATURAL(它是預設選項:使列保持資料庫驅動程式返回的形式)。
錯誤和錯誤處理
可移植指令檔的另一個難題是處理從各種資料庫處理程式返回的各種不同的錯誤訊息;某些資料庫對於程式化處理錯誤的支援能力很差,而其他一些資料庫則俱有非常豐富的錯誤程式碼。只要可行,PDO 將為您的指令檔提供一個統一的錯誤程式碼,從而使您不必為應對可移植性的這個方麵所累。當然,PDO 還會為驅動程式提供原生錯誤程式碼和錯誤訊息,以防您需要用它來進行診斷,或者錯誤程式碼對應不完整。
另一個困擾 PHP 資料庫延伸的一緻性問題是錯誤處理策略的一緻性:某些延伸會返回的錯誤程式碼需要您手動抓取錯誤字串,而其他一些延伸則隻是發出 PHP 警告。PDO 允許您從下列三種不同的錯誤處理策略中選擇一種:
請注意,與警告或異常相比,靜默模式針對執行時錯誤使用的資源最少,但是為了獲得該速度,您犧牲了一些簡單性,而變得有一點復雜。
統一錯誤程式碼表當前包括下列常量: 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,但是 PHP 支援的任何其他資料庫都不會這樣處理,從而導緻了可移植性問題。PDO 提供了一個驅動程式級內容 PDO_ATTR_ORACLE_NULLS,該內容會為其他資料驅動程式模擬此行為:
$dbh = new PDO('OCI:', 'scott', 'tiger');POD 的現狀和未來
PDO 現在仍相當不成熟,但是會快速成熟起來。在編寫本文之時,我在本文中提到的任何內容都能夠透過 PDO_OCI 驅動程式適用於 Oracle 8 或更高版本(在 Oracle 8.0 和 9.2 上測試過)。
已經計劃增加以下主要特性,在不久將可以使用:
我們需要您的回饋
如果您試用了 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 的電子郵件;您可能會發現如果首先與前麵的郵件清單聯係會更快得到答復。