Perl DBI 基础

DBI 数据类型

从某些方面来说,使用Perl DBI API 类似于使用第6章介绍的C 客户机库。在使用C 客户机库时,主要依靠指向结构或数组的指针来调用函数和访问与MySQL相关的数据。在使用DBI API 时,除了函数称为方法,指针称为引用外,也调用函数和使用指向结构的指针。
指针变量称为句柄,句柄指向的结构称为对象。
DBI 使用若干种句柄。它们往往通过表7-1所示的惯用名称在DBI 文件中引用。而惯用的非句柄变量的名称如表7 - 2所示。实际上,在本章中,我们并不使用每个变量名,但是,在阅读其他人编写的DBI 脚本时,了解它们是有用的。
表7-1惯用的Perl DBI 句柄变量名

名称 说明
$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 ";};
也可以用普通的风格定义一个子例程,并利用这个子例程的引用来设置这个句柄值:

除了在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科学计数法形式)。“0 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。
在前述的样例中, 我们提取每一行, 然后保存对它的引用。可以设想调用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 表中的成员姓名。可以告诉DBI 将选定列的值赋给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::mysql 1.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 "
结果如下:
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特定的属性而为可移植性花费努力,还是在可移植性的开销方面利用它们。

你可能感兴趣的:(sql,mysql,qq,脚本,perl)