名稱 | 說明 |
$dbh | 數據庫對象的句柄 |
$sth | 語句(查詢)對象的句柄 |
$fh | 打開文件的句柄 |
$h | “通用”句柄;其意義取決于上下文 |
表7-2 慣用的perl dbi 非句柄變量的名稱
名稱 | 說明 |
$rc | 從返回真或假的操作中返回的代碼 |
$rv | 從返回整數的操作中返回的值 |
$rows | 從返回行數的操作中返回的值 |
@ary | 查詢返回的表示一行值的數組(列表) |
一個簡單的dbi 腳本
讓我們從一個簡單腳本d um p _ member s開始,它舉例說明了dbi 程序設計中若干標準概念,如與mysql服務器的連接和斷開、檢索數據等。此腳本產生的結果為以制表符分隔形式列出的歷史同盟成員。這個格式本身并不讓人感興趣:在這里,了解如何使用dbi 比產生漂亮的輸出更為重要。
dump_members 如下:
要想自己試驗這個腳本,可以下載它(請參閱符錄a),或使用文本編輯器創建它,然后使之可執行,以便能運行。當然,可能至少需要更改一些連接參數(主機名、數據庫名、用戶名和口令)。本章中的其他dbi 腳本也是這樣。在參數缺省時,本章下載腳本的權限設置為只允許讀。如果您將自己的mysql用戶名和口令放在它們之中,我建議將它們保留為這種方式,以便其他人不能讀取這些值。以后,在7 . 2 . 8節“指定連接參數”中,我們將看到如何從選項文件中獲得這些參數,而不是將它們直接放在腳本中。
現在,讓我們逐行看完這個腳本。第一行是標準行,指出哪里可以找到perl 的指示器:
#! /usr/bin/perl
在本章將要討論的腳本中,每個腳本都包含這行;以后不再說明。此腳本中至少應該含有一個簡短的目的說明,這是一個好主意,所以下一行是一個注釋,給閱讀此腳本的人提供一個關于它做什么的線索:
# dump_members.dump historical league's membership list
從‘#’字符到行尾部的文本為注釋。有必要做一些練習,就是在整個腳本中編寫一些注釋來解釋它們如何工作。
接下來是兩個use 行:
use dbi;
use strict;
use dbi 告知perl 解釋程序它需要引入dbi 模塊。如果沒有這一行,試圖在腳本中做與dbi 相關的任何事,都將出現錯誤。不需要指出想要哪個dbd 級別的模塊。在連接數據庫時,dbi 會激活相應的模塊。
use strict 告知perl,在使用它們之前需要聲明變量。如果沒有use strict 行,也可以編寫腳本,但是,它有助于發現錯誤,所以建議始終要包括這行。例如,置為嚴格模式時,如果聲明變量$ my _ v a r,但是之后錯誤地用$mv_var 來訪問,則在運行這個腳本時,將獲得下面的消息:
global symbol "$mv_var" requires explicit package name at line n
這個消息會使您想,“怎么了?$ m v _ v a r?我從未使用過這種名稱的變量!”,然后,找到腳本中的第n行,看是什么問題,并改正它。如果不用嚴格模式, perl 不會給出$ m v _ v a r;將只是簡單地按具有un d e f(未定義的)值的該名稱創建一個新的變量,并毫無動靜地使用它,然后,您會莫名其妙腳本為什么不工作。
因為我們在嚴格模式下操作,所以我們將定義腳本使用的變量:
現在我們準備連接數據庫:
connect( ) 調用作為dbi->connect( ) 來調用,因為它是dbi 類的方法。不必真正知道它是什么意思;它只是一個使人頭痛的面向對象的行話(如果的確想知道,那么它意味著connect( ) 是“屬于”dbi 的一個函數)。connect( ) 有若干參數:
數據源。(經常調用的數據源名稱,或d s n。)數據源格式由要使用的特定dbd 模塊需求來確定。對于mysql驅動程序,允許的格式如下:
"dbi:mysql:db_name"
"dbi:mysql:db_name:host_name"
對于第一種格式,主機名缺省為localhost(實際上有其他允許的數據源格式,我們將在后面7 . 2 . 8節“指定連接參數”中討論)。“dbi”大寫沒關系,但是“ mysql”必須小寫。
用戶名和口令。
表示額外連接屬性的可選參數。這個參數控制dbi 的錯誤處理行為,我們指定的看起來有點奇怪的構造啟用了raiseerror 屬性。這導致dbi 檢查與數據庫相關的錯誤,并顯示消息,而且只要它檢測到錯誤就退出(這就是為什么在dump_members 腳本中的任何地方都沒有看到錯誤檢查代碼的原因; dbi 將它全部處理了)。7 . 2 . 3節“處理錯誤”包括了對錯誤響應的可選方法。
如果connect( ) 調用成功,則它返回數據庫句柄,我們分配給$dbh(如果connect( ) 失敗,通常返回un d e f。然而,因為我們在腳本中啟用了r a i s e e r r o r,所以connect( )不返回;但是,dbi 將顯示一條錯誤消息,并且在出現錯誤時退出)。
連接到數據庫后, dump_members 發布一條select 語句查詢來檢索全體成員列表,然后,執行一個循環來處理返回的每一行。這些行構成了結果集。
為了完成s e l e c t語句,首先需要準備,然后再運行它:
# issue query
$sth=$dbh->prepare("select last_name,first_name,suffix,email,"
"street,city,state,zip,phone from member order by last_name");
$sth->execute();
利用數據庫句柄調用prepare( );在執行前,它將sql 語句傳遞給預處理的驅動程序。實際上,在這里某些驅動程序做了一些有關這條語句的事情。其他驅動程序只是記住它,直到調用execute( ) 使這條語句被執行為止。從prepare( ) 返回的值是一個語句句柄$ s t h,如果出現錯誤,則為un d e f。在進一步處理與這條語句相關的所有內容時,都使用這個語句句柄。
請注意,指定的這個查詢沒有分號結束符。您無疑有這樣的(經過長時間使用mysql程序養成的)習慣,用‘ ;’字符終止sql 語句。然而,在使用dbi時,最好打破這個習慣,因為分號經常導致查詢出現語法錯誤而失敗。向查詢增加‘ / g’也類似,使用dbi 時不要這樣。
在調用一個方法而不用向它傳遞任何參數時,可以沒有這個圓括號。下列兩個調用是等價的:
$sth->execute();
$sth->execute;
我寧愿有圓括號,因為它使人感到這個調用看上去不像變量。您的選擇就可能不同了。
調用execute( ) 后,可以處理成員列表的行。在dump_members 腳本中,提取行的循環簡單地顯示了每行的內容:
fetchrow_array( ) 返回含有當前行的列值的數組,在沒有剩余的行時,返回一個空數組。這樣,此循環提取了由select 語句返回的連續行,并顯示列值之間用制表符分隔的每一行。在數據庫中null 作為undef 值返回到perl 腳本,但是將它們顯示為空字符串,而不是單詞“null”。
請注意,制表符和換行符(表示為‘ / t’和‘/ n’)括在雙引號中。在perl 中,只解釋出現在雙引號內的轉義符序列,不解釋出現在單引號內的轉義符序列。如果使用單引號,則輸出將為字符串“/ t”和“/ n”。
提取行的循環終止以后,調用finish( ) 告知dbi 不再需要語句句柄,并且釋放分配給它的所有臨時資源。實際上,除非只提取結果集的一部分(無論是設計的原因,還是因為出現一些問題),否則不需要調用finish( )。然而,在提取循環之后, finish( ) 始終是很保險的,我認為調用并執行finish( ),比區分何時需要,何時不需要更容易一些。
我們已經顯示完了全部成員列表,所以我們可以從服務器上斷開連接,并且退出:
$dbh->disconnect();
exit(0);
dump_members 示出了許多dbi 程序的大多數通用概念,而且不必了解更多的知識,就可以著手編寫自己的dbi 程序。例如,要想寫出一些其他表的內容,所需要做的只是更改傳遞給prepare( ) 方法的select 語句的文本。而且實際上,如果想了解這種技術的某些應用,可略過這部分,直接跳到7 . 3節“運行dbi”中討論如何生成歷史同盟一年一度的宴會成員列表程序和league 打印目錄的部分。然而,dbi 提供許多其他有用的功能。下一節介紹了一些,以便能夠在perl 腳本中看看如何完成比運行一條簡單的select 語句更多的事情。
處理錯誤
在dump_members 調用connect( )方法時,應該啟用raiseerror 錯誤處理屬性,以便這些錯誤用一條錯誤消息就能自動地終止相應的腳本。也可以用其他方式處理這些錯誤。例如,可以自己檢查錯誤而不必使用dbi。
為了查看如何控制dbi 的錯誤處理行為,我們來仔細查看一下connect( ) 調用的最終參數。下面兩個相關的屬性是raiseerror 和p r i n t e r r o r:
如果啟用r a i s e e r r o r(設為非零值),如果在dbi 方法中出現錯誤,則dbi 調用die( ) 來顯示一條消息并且退出。
如果啟用p r i n t e r r o r,在出現dbi錯誤時,dbi 會調用warn( ) 來顯示一條消息,但是相應腳本會繼續執行。
缺省時, raiseerror 是禁用的,而printerror 啟用。在此情況下,如果connect( )調用失敗,則dbi 顯示一條消息,而且繼續執行。這樣,如果省略connect( ) 的四個參數,則得到缺省的錯誤處理行為,可以如下檢查錯誤:
$dbh=dbi->connect($dsn,$user_name,$password) or exit (1);
如果出現錯誤,則connect( ) 返回undef 表示失敗,并且觸發對exit( ) 的調用。因為dbi 已經顯示了錯誤消息,所以您就不一定要顯示它了。
如果明確給出該錯誤檢查屬性的缺省值,可如下調用connect( )。
$dbh=dbi->connect($dsn,$user_name,$password,{raiseerror=>0,printerror=>1})
or exit (1);
這就需要更多的編寫工作,但是即使對不經意的讀者,處理錯誤行為也會更為明顯。
如果想自己檢查錯誤,并顯示自己的消息,應該禁用raiseerror 和p r i n t e r r o r:
變量$dbi::err 和$ dbi : :er r s t r,只用于所顯示的die( ) 調用中,有助于構造錯誤消息。它們含有mysql錯誤代碼和錯誤字符串,非常像c api 函數中的mysql_errno( ) 和mysql_error( )。
如果僅僅要dbi 處理錯誤,以便不必自己檢查它們,則啟用r a i s e e r r o r:
$dbh=dbi->connect ($dsn,$user_name,$password,{raiseerror=>1});
到目前為止,這是最容易的方法,并且是dump_members 帶來的。如果在腳本退出時,想要執行某種類型的清除代碼,啟用raiseerror 可能是不恰當的,盡管在這種情況下,可以重新定義$sig{_die_} 句柄,可以做想做的事情。
避免啟用raiseerror 屬性的另一個原因是dbi 在它的消息中顯示技術信息,如下:
disconnect(dbi::db=hash(0x197aae4)invalidates 1active statement.either
destroy statement handles or call finish on them before disconnecting.
對于編程者來說,這是好的信息,但對普通用戶可能沒有什么意義。在此情形,最好自己檢查錯誤,以便可以顯示對期望使用這個腳本的人更有意義的消息。或者也可在這里考慮重新定義$sig{_die_} 句柄。這樣可能很有用,因為它允許啟用raiseerror 來使錯誤處理簡單化,而不是用自己的消息替換dbi 給出的缺省錯誤消息。為了提供自己的_die_ 句柄,可在執行任何dbi 調用以前,進行下面的工作:
$sig{_die_}=sub{die "sorry,an error occurred/n";};
也可以用普通的風格定義一個子例程,并利用這個子例程的引用來設置這個句柄值:
除了在connect( ) 調用中逐字傳遞錯誤處理屬性之外,還可以利用散列定義它們,并傳遞對這個散列的引用。有人發現以這種方式準備屬性設置使腳本更容易閱讀和編輯,但是在功能上這兩種方法是相同的。下面是一個說明如何使用屬性散列的樣例:
下面的腳本dump_members2 舉例說明了當要自己檢查錯誤并顯示自己的消息時,如何編寫腳本。dump_member2 處理和dump_members 一樣的查詢,但是明確地禁用printerror 和r a i s e e r r o r,然后測試每個dbi 調用的結果。如果出現錯誤,在退出以前,腳本調用了子例程bail_out( ) 顯示消息及$dbi::err 和$dbi::errstr 的內容:
除了bail_out( ) 是退出而不是返回到調用者以外, bail_out( ) 類似于我們在第6章中為編寫c 程序使用的print_error( ) 函數。每次想顯示錯誤消息時, bail_out( ) 解除了寫出$dbi::err 和$dbi::errstr 名稱的麻煩。同樣,通過封裝顯示到子例程的錯誤消息,可更改子例程使整個腳本中錯誤消息的格式一致。
dump_member2 腳本在提取行循環的后面有一個測試,這是dump_members 所沒有的。因為如果在fetchrow_array( ) 中出現錯誤,dump_members2 不會自動地退出,所以人們判斷循環是因為結果集讀取完成而終止(正常終止),還是因為出現錯誤而終止做出確定是很困難的。當然,任何一種方式,循環都將終止,但是如果出現錯誤,則將刪截腳本的輸出。如果沒有錯誤檢查,運行該腳本的人將無法知道是否有錯!如果自己檢查錯誤,應該檢查提取循環的結果。
處理不返回結果集的查詢
d e l e t e、insert、replace和update等執行后不返回行的語句比s e l e c t、describ、explain 和show 等執行后返回行的語句的處理相對要容易一些。為處理一條非select 語句,利用數據庫句柄,將它傳遞給do( )。do( ) 方法在一個步驟內準備和執行該查詢。例如,開始輸入一個新的成員, marcis brown,終止日期為2002 年6 月3 日,可以這樣做:
do( ) 方法返回涉及行的計數,如果出現錯誤,則返回un d e f。因為各種原因,可能出現錯誤(例如,這個查詢可能是畸形的,或可能沒有訪問這個表的權力)。對于非undef 的返回,注意那些沒有受到影響的行的情況。當這種情況發生時, do( ) 不返回數字0;而是返回字符串“0 e 0”(0的perl科學計數法形式)?!? e 0”在數值上等價于0,但是,在條件測試中將其視為真,以便可以將其與早期的undef 區別。如果do( ) 返回0,則區分是出現了錯誤( un d e f)還是“沒有受到影響的行”這兩種情況將更困難。使用下面的兩個測試之一可以檢查錯誤:
if (!defined ($rows)){#error}
if (!$rows) {#error}
在數值環境中,“0 e 0”與0 等價。下面的代碼將正確地顯示$rows 的任何非undef 值的行數:
也可以用printf( ) 使用‘% d’格式顯示$row 來強制進行隱含的數字轉換:
do( ) 方法等價于后跟execute( ) 的prepare( )。前面的insert 語句可以不調用do( ),如下發布:
處理返回結果集的查詢
本章提供了有關實現select 查詢中提取行循環的若干選項的詳細信息(或其他類似于select 的返回行的查詢,如describ e、explain 和s h o w )。還討論了如何獲得結果中行數的計數值,如何處理不需要循環的結果集,以及如何一次檢索整個結果集的全部內容等。
1. 編寫提取行的循環
dump_members 腳本利用dbi 方法的標準序列檢索數據:prepare( ) 使驅動程序處理查詢,execute( ) 開始執行這個查詢, fetchrow_array( ) 提取結果集中的每一行, finish( ) 釋放與這個查詢相關的資源。
prepare( )、execute( ) 和finish( ) 是處理返回行的查詢中非常標準的部分。然而,對于提取的行,fetchrow_array( ) 實際上只是若干方法中的一種(請參閱表7 - 3)。
方法名 | 返回值 |
fetchrow_array( ) | 行值的數組 |
fetchrow_arrayref( ) | 對行值數組的引用 |
fetch( ) | 與fetchrow_arrayref( ) 相同 |
fetchrow_hashref( ) | 對行值的散列引用,列名鍵索引 |
下面的例子說示出了怎樣使用每個提取行方法。這些例子在整個結果集的行中循環,對于每一行,顯示由逗號分隔的列值。在某些情況下,編寫這些顯示代碼還有一些更有效的方法,但是這些例子是以能夠說明訪問單個列值的語法的方式編寫的。
可如下使用fetchrow_array( ):
對fetchrow_array( ) 的每個調用都返回行值數組,不再有行時,返回一個空數組。
選擇將返回值分配給數組變量,可以在一組標量變量中提取列值。如果想使用比$ a r y [ 0 ]、$ary[1] 等更有意義的變量名,就可以這樣做。假設要在變量中檢索名稱和電子郵件值,可使用fetchrow_array( ),可以如下選擇并提取行:
當然,在以這種方式使用一列變量時,必須保證查詢按正確的次序選擇列。dbi 不關心select 語句指定列的次序,所以正確地分配變量是您的職責。在提取行時,使用一種稱為參數約束的技術,也可以使列值自動分配給單獨的變量。
fetchrow_arrayref( ) 類似于fetchrow_array( ),但不返回包含當前行的列值的數組,而是返回這個數組的引用,在沒有乘余行時,返回un d e f。如下使用:
通過數組引用$ary_ref 訪問數組元素。這類似于引用指針,所以使用了$ary_ref->[$i] 而不是$ a r y [ $ i ]。要想引用整個數組,就要使用@{$ary_ref} 結構。
fetchow_arrayef( ) 不適合在列表中提取變量。例如,下面的循環不起作用:
實際上,只要fetchrow_arrayref( ) 提取一行,這個循環就能正確地運行。但是在沒有更多的行時, fetchrow_arrayref( ) 返回un d e f,并且@{undef} 不合法(它有些像在c 程序中試圖廢棄一個null 指針)。
提取行的第三個方法fetchrow_hashref( ),如下使用:
對fetchrow_hashref( ) 的每個調用都返回一個按列名索引的行值散列的引用,在沒有更多的行時,返回un d e f。在此情況下,列值不按特定的次序出現; perl 散列的成員是無序的。然而,散列元素是按列名索引的,所以$hashref 提供了一個單獨的變量,可通過它按名稱訪問任何列值。這使得能按任意需要的次序來提取值(或者它們中的任何子集),而且不必知道select 查詢檢索的列的次序。例如,如果想訪問名稱和電子郵件域,可以如下進行:
如果希望將一行值傳遞給某個函數而又不需要這個函數知道select 語句中指定列的次序時,fetchrow_hashref( ) 是非常有用的。既然如此,可以調用fetchrow_hashref( ) 來檢索行,并且編寫一個使用列名訪問來自行散列值的函數。
如果使用fetchrow_hashref( ),請記住下列警告:
如果性能很重要,則fetchrow_hashref( ) 并不是最好的選擇,因為它沒有fetchrow_array( ) 或fetchrow_arrayref( ) 的效率高。
作為散列鍵值使用的列名具有與select 語句中寫出時相同的字符。在mysql中,列名不區分大小寫,所以此查詢也是這樣,不管以大寫字母還是小寫字母給出列名,查詢結果都是一樣的。但是perl 散列索引名是區分大小寫的,這可能會帶來一些問題。為了避免潛在的大小寫不匹配問題,可通過傳遞name_lc 或name_uc 屬性,告知fetchrow_hashref( ) 強迫列名為大寫或小寫:
散列對每個唯一的列名含有一個元素。如果正在執行從多個具有重疊名稱的表中返回列的連接,則不能訪問所有的列值。例如,如果發布下面的查詢, fetchrow_hashref( )將返回只有一個元素的散列:
select a.name from a,b where a.name=b.name
2. 確定查詢返回的行數
如何知道select 或類似于select 的查詢返回的行數?一種方法是,當提取它們時,計算這些行的數量。實際上,這是知道select 查詢返回多少行的唯一方便的方法。使用mysql驅動程序,可以在調用execute( ) 后利用語句句柄調用rows( ) 方法,但是這對其他數據庫引擎并不方便。而且即使就mysql來說,如果已經設置了mysql_use_result 屬性,rows( ) 也不能返回正確的結果,直到提取了所有行(有關的詳細信息,請參閱附錄g)。所以只能如提取行一樣對它們進行計數。
3. 提取單行的結果
如果結果集只含單個行,則不需要運行循環來獲得結果。假設要編寫得出歷史同盟成員當前數量的腳本count _ member s。完成查詢的代碼如下所示:
select 語句只返回一行,所以不需要循環;我們只調用fetchrow_array( ) 一次。另外,因為我們只選擇一列,所以甚至不需要將返回值分配給數組。當在標量環境中(單個值而不是所期望的一列)調用fetchrow_array( ) 時,它返回這個行的第一列,如果沒有更多的有效行,則返回un d e f。
另一種期望最多有一個記錄的查詢是一個含有limit 1來約束返回的行數的查詢。其一般的用法是返回特定列含有最大或最小值的行。例如,下面的查詢給出最近出生的總統姓名和出生日期:
必須無提取循環的其他類型的查詢利用max( ) 或min( ) 來選擇單個值。但是在所有這些情況下,獲得單個行結果的一種更容易的方法就是使用數據庫句柄方法selectrow_array( ),它結合了prepare( )、execute( ) 并在單個調用中提取行。它返回一個數組(而不是一個引用),如果出現錯誤,則返回一個空數組。前一例子可利用selectrow_array( ) 編寫如下:
4. 處理完整的結果集
在使用提取循環時, dbi 不提供在結果集中隨意查找的方法,或以任何次序而不是以循環返回的次序來處理行。同樣,提取行以后,如果沒有保存,前一行會丟失。這種做法并不一定合適以下情況:
以非連續的次序處理行。考慮一種情況,想以歷史同盟的president 表中列出的美國總統為主體,進行一些測驗。如果希望每次測驗時都以不同的次序提出問題,則可以從president 表中選擇所有行。然后,可能以任意的次序提取行來改變與所問問題有關的總統的次序。要想任意地提取一行,就必須同時訪問所有的行。
只使用返回行的子集,對其進行隨機選擇。例如,當問及總統出生地時,要想出現多個選擇的問題,則可以隨便地提取一行來選擇總統(正確的答案),然后再從取來干擾的選擇中提取若干其他行。
即使確實以連續的次序去處理,也想緊緊抓住整個結果集。如果想經過這些行進行多個傳遞,這可能是必需的。例如,在統計計算中,可能先瀏覽一遍結果集,來估計數據的一些通用數字屬性,然后再次檢查這些行,來實現更加明確的分析。
可以用幾個不同的方式作為一個整體訪問結果集??梢酝瓿蛇@個常見的提取循環,并在提取它時保存每一行,可以使用一次返回整個結果集的方法。無論哪種方法都以在結果集中包括一行一行的矩陣作為結束,和選擇的列一樣多。可以以任何次序任意多次地處理矩陣的
元素。下面的討論說明這兩種方法。
使用提取循環來捕獲結果集的一種方法是使用fetchrow_array( ) 并保存對這些行引用的數組。除了保存所有的行,然后顯示矩陣舉例說明了如何確定矩陣中的行數和列數,及如何訪問矩陣的個別成員以外,下面的代碼和dump_members 中提取和顯示的循環作用是一樣的。
在確定矩陣的維數時,必須首先確定行數,因為無論這個矩陣是否為空,都可能計算列數。如果$rows 為0,則這個矩陣為空,并且$cols 也為0。否則,列數可能作為行數組中的元素數量來計算,用語法@{$matrix[$i]} 來整體訪問行$ i。
在前述的樣例中, 我們提取每一行, 然后保存對它的引用??梢栽O想調用fetchrow_arrayref( ) 而不是直接地檢索行引用可能更有效率:
它不能正常工作,因為fetchrow_arrayref( ) 重新使用了引用指向的數組。結果矩陣是一個引用的數組,數組中的每個元素都指向相同行—最后檢索的行。因此,如果想一次提取一行,則要使用fetchrow_array( ) 而不是fetchrow_arrayref( )。
另一個選擇是使用提取循環,可以使用返回整個結果集的dbi 方法中的一個。例如,fetchall_arrayref( ) 返回對引用數組的引用,數組的每個元素都指向結果集中某行。這非常簡單,但很有效,這個返回值是對矩陣的引用。要想使用fetchall_arrayref( ),則調用prepare( )和execute( ),然后如下檢索結果:
如果結果集為空,則fetchall_arrayref( ) 返回一個對空數組的引用。如果出現錯誤,則結果為un d e f,所以如果沒有啟用r a i s e e r r o r,則在開始使用它以前,要確保檢查返回值。
行數和列數由矩陣是否為空來確定。如果想作為一個數組訪問這個矩陣的整個行$ i,應該使用語法@ { $ m a t r i x _ r e f - > [ $ i ] }。
使用fetchall_arrayref( ) 來檢索結果集當然比編寫一個提取行的循環要更簡單一些,盡管訪問數組元素的語法是一個小技巧。一個與fetchall_arrayref( ) 方法相類似,但卻做了更多工作的方法是selectall_arrayref( )。這個方法為您完成了整個prepare( )、execute( )、提取循環、finish( ) 序列。為了使用selectall_arrayref( ),應該利用數據庫句柄直接將查詢傳遞給它:
5. 檢查null 值
從數據庫中檢索數據時,可能需要區分值為null 、為0 或者為空字符串的列。因為dbi 返回null 列值作為un d e f,所以這種區分是容易的。然而,必須確保使用正確的測試。如果試用下面的代碼段,則它所有三次顯示都為“ f a l s e !”:
而且,對于這兩個測試,這段代碼都顯示“ f a l s e !”:
下面這段代碼效果相同:
要想區分null 列值和非null 列值,則使用defined( )。知道了沒有出現null 值之后,使用適當的測試可以在其他類型值之間加以區分。例如:
以適當的次序完成這些測試是很重要的,因為如果$col_val 為空字符串,則第二個和第三個比較就都為真。如果顛倒比較的次序,則會錯誤地將空字符串標識為0。
引用問題
迄今為止,我們已經利用引用字符串以最基本的方式構造了查詢。在引用的字符串含有引用值時,會在perl 詞匯一級產生問題。在插入或者選擇含有引號、反斜杠或二進制數據的值時,在sql 中也可能出問題。如果指定一個查詢作為perl 引用的字符串,則必須避免在查詢字符串本身中出現引用字符:
perl 和mysql都允許用單引號或雙引號引用字符串,所以混合使用引用字符有時可以避免這種無法引用引用字符自身的情況:
然而,在perl 中,這兩種類型的引號并不等價。只有在雙引號內部才解釋為變量引用。因此,當想通過在查詢字符串中嵌入變量引用來構造查詢時,單引號并不是非常有用的。例如,如果$var 的值為14,則下面的兩個字符串并不等價:
兩個字符串的解釋如下所示;顯然,第一個字符串與希望傳遞給mysql服務器的內容更為相像:
用雙引號來引用字符串的另一個選擇是使用qq{} 結構,它告訴perl 在‘q q {’和‘}’之間的每個字符都要看作為雙引號括起的字符串(兩個q 表示“雙引號”)。例如,下列兩行是等價的:
使用qq{} 時,構造查詢不用過多考慮引號的問題,因為可以在這個查詢字符串內自由地使用引號(單引號或雙引號),而不用避開它們。此外,還解釋了變量引用。qq{} 的這兩種特性可用下面的insert 語句來說明:
不一定使用‘ {’和‘ }’作為qq 的分隔符。其他格式,如qq( ) 和q q / /,也可以使用,只要封閉的分隔符不出現在字符串內即可。我喜歡用q q { },因為‘ {’不像‘)’或‘/’會出現在查詢的文本內,并且在查詢字符串的結尾也可能有問題。例如,‘)’出現在所顯示的insert 語句的內部,所以qq( ) 對于引用查詢字符串來說不是一個有用的結構。
qq{} 結構能跨行,如果想讓查詢字符串在perl 代碼中醒目,這很有用:
如果希望將查詢格式分為多個行,從而使它的可讀性更強,這樣也很有用。例如,dump_members 腳本中的select 語句如下:
用q q { }編寫如下:
雙引號字符串也可以跨行。但是,對于編寫多行的字符串,我更喜歡用q q { }。我發現當在一行中看到不匹配的引號時,我就自然地去想,“這會是語法錯誤嗎?”,然后,我就會浪費時間去尋找相匹配的引號。
qq{} 結構在perl 詞匯級注意了引用的問題,因此可將引號容易地放到字符串內,而不會使perl 搞混它們。然而,還必須考慮sql 級的語法。考慮向member 表中插入一條記錄:
do( ) 發送給mysql的字符串如下所示:
這是不合法的sql 語句,因為在單引號字符串內出現了單引號。在第6章中,我們遇到過類似的引用問題。在那里,我們使用mysql_escape_string( ) 來處理這個問題。dbi 提供了一個類似的機制—在一條語句中,對想按字面使用的每個引用值,都調用quote( ) 方法,并使用它的返回值。
前面的例子可編寫如下:
現在,do( ) 發送給mysql的字符串如下所示,具有出現在引用字符串內的可能對服務器轉義的引號:
請注意,在查詢字符串中引用$last 和$first 時,不要增加括起來的引號; quote( ) 方法支持它們。如果增加了引號,則查詢將出現過多的引號,如下面的例子所示:
這些語句產生下面的輸出:
占位符和參數約束
在前面各節中,我們通過把要插入或選擇的值作為選擇標準,直接放在查詢字符串中構造了查詢。不一定非要這樣做。dbi允許在查詢字符串內部放置一些稱為占位符的特殊標記符,然后,在執行該查詢時,將這些值代替那些標識符來使用。這樣做的主要原因是提高性能,特別是在循環中反復執行某個查詢的時候。
為了說明占位符如何工作,舉例說明。假設學校新學期剛開始,打算清理學分薄的student 表,然后利用包含在文件中的一列學生姓名將其初始化,使其包含新學生。不用占位符,可以如下這樣刪除現有表的內容,并裝入新的姓名:
這樣做效率很低,因為insert 查詢的基本格式每次都是相同的,并且在整個循環中,do( ) 每次都調用prepare( ) 和execute( )。在進入這個循環以前,只調用一次prepare( ) 來設置insert 語句,并且在這個循環內部只調用execute( ),這樣做效率更高一些。只調用一次prepare( ),可避免其他多次調用。dbi 允許我們這樣做:
請注意這個insert 查詢中的‘ ?’就是一個占位符。調用execute( ) 時,將查詢發送給服務器,傳遞這個值來代替占位符。一般來說,如果發現在循環內部調用了do( ),應該在循環前調用prepare( ),并在這個循環內部調用execute( ) 更好一些。
有關占位符的一些注意事項:
在查詢字符串內,不要在引號中封裝占位符字符。如果這樣做,不能識別為占位符。
不要使用quote( ) 方法來指定占位符的值,否則將在插入的值中得到額外的引號。
在查詢字符串中可以有一個以上的占位符,但是要確保占位符的標記符與傳遞給execute( ) 的值一樣多。
每個占位符都必須指定一個單獨的值,而不是一列值。例如,不能運行這樣的語句:
為了將null 指定為占位符,應該使用un d e f。
不要對關鍵字使用占位符。這樣會出問題,因為占位符的值是由quote( ) 自動處理的。
關鍵字將被放在引號括起來的查詢中,因此,這個查詢會由于語法錯誤而失敗。
除了在循環中提高效率以外,對于某些數據庫引擎,可以從占位符的使用中獲得其他的性能好處。某些引擎高速緩存了準備好的查詢,以及為有效地運行這個查詢所生成的計劃。也就是說,如果以后這個服務器收到同樣的查詢,則它可以再次使用相應的計劃而不用生成。查詢高速緩存特別有助于復雜的select 語句,因為可能需要花費時間生成較好的執行計劃。占位符提供了一個在高速緩存中尋找查詢的好機會,因為它們使查詢比直接在查詢字符串中嵌入指定的列值來構造查詢更通用。對于mysql,在這種方式下,占位符并不提高性能,因為沒有高速緩存查詢。然而,可能仍想使用占位符編寫自己的查詢;如果偶然將dbi 腳本傳遞給支持查詢高速緩存的引擎,則這個腳本比沒有占位符時運行效率更高。
在查詢運行時,允許在查詢字符串中用占位符代替這些值。換句話說,可以參數化這個查詢的“輸入”。在提取行而不必將值賦給變量時,dbi 也提供一個稱為參數約束的輸出操作,允許通過檢索自動進入這些變量的列值使“輸出”參數化。
假設有一個查詢,檢索member 表中的成員姓名??梢愿嬖Vdbi 將選定列的值賦給perl變量。在提取行時,變量利用相應的列值自動進行更新。下面是一個例子,說明如何將這些列約束到變量上,然后在提取循環中訪問它們:
bind_col( ) 的每個調用都應該指定一個列號和一個希望與該列相聯的變量的引用。列號從1開始。bind_col( ) 應該在execute( ) 之后調用。
還有一種選擇,就是單獨調用bind_col( ),可以在bind_columns( ) 的單個調用中傳遞全部變量引用:
指定連接參數
建立服務器的連接的最直接的方法為,調用connect( ) 方法時指定所有連接參數:
如果遺漏連接參數,則dbi 做下面的事情:
如果未定義數據源或未定義空字符串,則使用dbi_dsn 環境變量。如果未定義用戶名和口令,則使用dbi_user 和dbi _ pass 環境變量(但如果它們為空字符串則不使用)。在windows 下,如果未定義用戶名,則使用user 變量。
如果遺漏了主機名,其缺省值為localhost。
如果將用戶名指定為undef 或空字符串,則其缺省為unix 的登錄名稱。在windows 下,用戶名缺省為odbc。
如果將口令指定為undef 或空字符串,則不傳送口令。
通過將某些選項添加到字符串的初始部分,每個都在分號前面,可以在數據源中指定這些選項。例如,可以使用mysql_read_default_file 選項來指定一個選項文件的路徑名:
當執行這個腳本時,它將從這個文件中讀取連接參數。假設/ u/ paul /.my.cnf 含有下面的內容:
然后connect( ) 調用試圖連接到p i t - v i per-snake-net 上的mysql服務器,并且用口令secret 及用戶名paul 連接。如果想允許具有正確地設置選項文件的任何人使用您的腳本,則像這樣指定數據源:
$env{home} 含有用戶運行這個腳本的主目錄的路徑名,所以這個腳本使用的主機名、用戶名和口令將會從每個用戶自己的選項文件中抽取出來。以這種方式編寫腳本,不必在這個腳本中逐字地嵌入連接參數。
還可以使用mysql_read_default_group 選項,來指定一個選項文件組。這自動地導致讀取用戶的.my.cnf 文件,并且除了[client] 組以外,還允許讀取一個指定的選項組。例如,如果在dbi 腳本中具有指定的選項,則可以將它們列在[dbi] 組中,然后以如下方式使用數據源值:
mysql_read_default_file 和mysql_read_default_group 需要mysql3.22.10 或更新的版本,以及dbd::mysql1.21.06 或更新的版本。有關指定的數據源字符串的選項的詳細信息,請參閱附錄g。有關mysql選項文件格式的詳細信息,請參閱附錄e。
使用選項文件并不防礙在connect( ) 調用中指定連接參數(例如,如果想這個腳本作為特殊的用戶來連接)。在connect( ) 調用中指定的任何明確的主機名、用戶名和口令值都將覆蓋在選項文件中找到的連接參數。例如,想要腳本從命令行中分析- - host、--user 和--password 選項,并使用那些值,如果給定,則優先于在選項文件中發現的任何內容。這是有用的,因為它是標準的mysql客戶機操作的方式。dbi 腳本將因此符合它的行為。
對于在本章中我們開發的保留在命令行中的腳本,我將使用一些標準的連接設置代碼及卸載代碼。我只在這里說明它一次,以便我們可以將精力集中在每個腳本的主體上,我們編寫如下代碼:
這個代碼初始化dbi,在命令行中查找連接參數,然后使用命令行中的或者在用戶運行這個腳本的- /.my.cnf 文件中所找到的參數,連接到mysql服務器。如果在主目錄中設置.my.cnf 文件,則當運行這個腳本時,不一定要輸入任何連接參數(請記住,設置這種方式,以便沒有其他人讀取這個文件。有關的指導請參閱附錄e)。
我們腳本的最后部分也類似于從腳本到腳本;它簡單地終止這個連接并退出:
$dhb->disconnect();
exit (0);
當我們讀到web 程序設計的部分,即7 . 4節“在web 應用程序中使用dbi”時,將修改一些這個連接設置代碼,但是基本的思想是類似的。
調試
當想調試有故障的dbi 腳本時,通常使用兩項技術,即單獨使用一個或一前一后地配合使用。首先,在腳本的整個過程中編寫顯示語句。它允許將自己調試的輸出設計為想要的方式,但必須手工地增加語句。其次,可以使用dbi 的內建跟蹤能力。這更加通用,但也更加
系統,而且它在打開以后,則會自動地出現。dbi 跟蹤也說明一些除此以外就無法獲得的有關驅動程序的操作信息。
1. 使用顯示語句調試
在mysql郵件清單中,常見問題之一是:“有一個查詢,當我在mysql中執行它時運行得很好,但是它不能在我的dbi 腳本中工作,怎么回事?”尋找發布不同查詢的dbi 腳本和這個發問者所期望的一樣是很平常的。如果在執行它之前顯示查詢,則可能會驚異地看到真
正發送到這個服務器上的內容。假設將一個查詢鍵入到mysql中(沒有終止的分號)
然后,在dbi 腳本中試著做相同的事情:
盡管它是同樣的查詢,但它不能工作。不是嗎?試著顯示:
print "$query/n"
結果如下:
insert member (last_name,first_name,expiration)
values(brown,warcia,2002-6-3)
從這個輸出中,可以看到是您忘記了values( ) 列表中這些列值前后的引號。指定查詢的正確方法如下:
或者,可以使用占位符指定查詢,并傳遞這些值,直接插入到do( ) 方法中:
不幸的是,當做這些的時候,使用顯示語句不能看到完整查詢的樣子,因為直到調用d o ( )才能估計占位符的值。當使用占位符時,跟蹤可能對調試方法更有幫助。
2. 使用跟蹤調試
當試圖查出腳本不能正確工作的原因時,可以告知dbi 來生成跟蹤(調試)信息。跟蹤級別范圍從0(關閉)到9(最多信息)。一般來說,跟蹤級別1和2 是最有用的。級別2 跟蹤說明正在執行的查詢文本(包括占位符替換的結果)、調用quote( ) 的結果等等。這可能對捕獲問題有極大的幫助。
使用trace( ) 方法,可以從獨立的腳本內部控制跟蹤,或者可以設置dbi_trace 環境變量來影響所運行的所有dbi 腳本的跟蹤。
要想使用trace( ) 調用,則傳遞一個跟蹤級別參數,并可以有選擇地再傳遞一個文件名。如果沒有指定文件名,則所有的跟蹤輸出到stderr 中;否則,它就轉到這個命名的文件中。一些樣例如下:
當調用dbi->trace( ) 時,跟蹤所有的dbi 操作。一個更精細的方法是,可以用獨立的處理級別啟用跟蹤。當沒想好腳本中問題的位置,并對在那點出現的每件事的跟蹤輸出不想插手時,這是有幫助的。例如,如果特定的select 查詢有問題,則可以跟蹤與這個查詢相關的語句句柄:
如果對任何trace( ) 調用指定一個文件名參數,則無論對dbi 作為整體還是單獨的句柄,所有的跟蹤輸出都要到那個文件中。
要想對運行的所有dbi 腳本全部都打開跟蹤,則從命令解釋程序中設置dbi_trace 環境變量。它的語法取決于使用的命令解釋程序:
value 的模式和所有命令解釋程序的模式一樣:數字n表示在級別n打開跟蹤到s t d e r r中;文件名打開級別2 跟蹤到這個命名的文件,或n=file_name 打開級別n跟蹤到這個命名的文件中。下面的樣例使用了csh 語法:
如果打開跟蹤到命令解釋程序中的文件,則確保一旦解決了這個問題,就將它關閉。將調試輸出增加到這個跟蹤文件中,而不用重寫它,所以如果不小心,則這個文件可能變得非常大。極其不好的想法是在命令解釋程序的啟動文件(如.cshrc、.lonin 或. p r o f i l e)中定義dbi _ t r a c e!在unix 下,可以使用下面兩個命令( csh 語法)之一關閉跟蹤:
% setenv dbi_trace 0
% unsetenv dbi_trace
對于s h、ksh 或b a s h,這樣做:
$ dbi_trace=0
$export dbi_trace
在windows 操作系統中,可以使用下面兩個命令之一關閉跟蹤:
c:/>unset dbi_trace
c:/>set dbi_trace=0
使用結果集元數據
可以使用dbi 來獲得訪問結果集元數據——也就是有關由查詢選擇行的描述信息。訪問與結果集生成的查詢所相關的語句句柄的屬性來獲得這個信息。提供這些屬性中有一些是作為可用于橫跨所有數據庫驅動程序的標準dbi 屬性(如n u m _ o f _ f i e l d s,結果集中列的數量)。另外一些是mysql特定的,由dbd::mysql所提供的dbi 的mysql驅動程序。這些屬性,如mysql_max_length 告知了每列值的最大寬度,不能用于其他數據庫引擎。要想使用任何mysql特定的屬性,都必須冒著使腳本不可移植到其他數據庫的危險。另一方面,它們可以使它更容易地獲得想要的信息。
必須在適當時候請求元數據。一般來說,直到調用prepare( ) 和execute( ) 之后,結果集屬性才能用于select 語句。除此之外,在調用finish( ) 之后,屬性可能變為無效。
讓我們來看看如何使用mysql的一個元數據屬性mysql_ m a x _ l e n g t h,與保留查詢列名的dbi 級別的name 屬性一起使用。我們可以將這些屬性提供的信息合并起來,編寫一個腳本b o x _ o ut,它以交互模式運行mysql客戶機程序時獲得的相同邊框風格,從select 查詢產生輸出。box_out 的主體如下(可以用任何其他的語句替換select 語句;編寫輸出的例程獨立于特定的查詢):
用execute( ) 將這個查詢初始化之后,我們獲得了所需的元數據。$sth->{name} 和$ s t h ->{mysql_max_length} 給出了列名和每列值的最大寬度。為了在這個查詢中為列命名,每個屬性值都引用了一個數組,這個數組含有結果集每列中的一個值。
剩余的計算非常類似于在第6章中開發的客戶機程序5中所使用的那些內容。例如,為避免偏離輸出,如果列的名比該列中任何數據值都寬,則我們要向上調整列的寬度值。
輸出函數print_dashes( ) 和print_row( ) 代碼編寫如下,它們也類似于客戶機程序5中相應的代碼:
box_out 的輸出如下:
我們的下一個腳本使用了列元數據來產生不同格式的輸出。這個腳本s h o w _ member,允許快速瀏覽歷史同盟成員項目,而不用輸入任何查詢。給出成員的姓,它就這樣顯示所選擇的項目:
使用成員資格號碼,或者使用與若干姓相匹配的模式也可以調用s h o w _ member s。下面的命令說明成員號碼為2 3的項目,和以字母“c”開始的姓的成員項:
show_member 腳本的主體如下所示。它使用了name 屬性來確定輸出的每行所使用的標號和num_of_fields 屬性,找出這個結果集含有的列數:
無論區域是什么, show_member 的目的都是說明一個項目的全部內容。通過使用select * 來檢索所有的列和name 屬性來看看它們是什么,即使從member 表中增加或刪除列,這個腳本也會工作而不用做修改。
如果不檢索任何行就想知道一個表含有哪些列,則可以發布下面這條查詢:
select * from tbl_name where 1=0
以正常方式調用prepare( ) 和execute( ) 之后,可以從@{$sth->{name}} 中得到列名。然而,請注意,盡管使用“空”查詢的這個小竅門可以在mysql下運行,但是它不可移植,而且并不是對所有的數據庫引擎都可以工作的。
有關dbi 和dbd::mysql所提供屬性的詳細信息,請參見附錄g。它完全可以使您確定是想通過避免mysql特定的屬性而為可移植性花費努力,還是在可移植性的開銷方面利用它們。
新聞熱點
疑難解答