##Class::DBI 翻 译:Joe Jiang 出 处:中国Perl协会 FPC(Foundation of Perlchina) 原 名:Class::DBI 作 者:Tony Bowden 原 文:http://www.perl.com/pub/a/2002/11/27/classdbi.html 发 表:November 27, 2002 Perlchina提醒您:请保护作者的著作权,维护作者劳动的结晶。 最近perl.com上的几篇文章(包括Phasebook设计模式)都讨论了Perl代码和数据库打交道的问题。Terrence Brannon的DBIx::Recordset一文试图展示数据库相关的程序也可以更加简单和易于维护。这篇文章是要用Class::DBI来使得这个努力更进一步。

Class::DBI奖励懒惰和简单。目标是使简单的数据库操作几乎不用编程,同时使困难的变得有可能。对很多简单的数据库应用来说,它使我们完全不用编写SQL,另一方面它也不强迫你用很复杂的数据结构来表示一个复杂查询。如果你确实需要原始SQL的功能或表达能力,它也会适时的给你让路。

最容易了解Class::DBI的方法就是用它来建立一个例子程序。这篇文章里面我要做个工具来分析我的电话帐单。

Data::BT::PhoneBill(可在CPAN下载)给我们一个从BT的网站下载电话帐单的方法。有了这个模块和一些最近的通话帐单条目,我们就可以用数据库来存储详细信息以备分析。

Class::DBI的基本概念是数据库中的每个表都有相应的类。尽管每个类都可以自己做连接(数据库)相关的事情,最好还是有个类来把这些事情封装起来。所以我们要建立数据库并为应用程序建立基类:

package My::PhoneBill::DBI; use base 'Class::DBI';

PACKAGE->set_db('Main', 'dbi:mysql:phonebill', 'u/n', 'p/w');

1; 我们只是从Class::DBI继承并用'set_db'方法来建立数据库连接。目前这就是我们在这个类里面需要做的事情,下面我们开始建立用于存储通话信息的表: CREATE TABLE call ( callid MEDIUMINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, number VARCHAR(20) NOT NULL, destination VARCHAR(255) NOT NULL, calldate DATE NOT NULL, calltime TIME NOT NULL, type VARCHAR(50) NOT NULL, duration SMALLINT UNSIGNED NOT NULL, cost FLOAT(8,1) ); 为这个我们要建立相应的类: package My::PhoneBill::Call; use base 'My::PhoneBill::DBI';

PACKAGE->table('call'); PACKAGE->columns(All => qw/callid number destination calldate calltime type duration cost/);

1; 我们从基类来继承连接信息,并声明我们要用的表和它包含的列。现在我们要开始填充表里面的数据了。 我们建立了一个简单的名为"populate_phone_bill"的脚本:

#!/usr/bin/perl

use Data::BT::PhoneBill; use My::PhoneBill::Call;

my $file = shift or die "Need a phone bill file"; my $bill = Data::BT::PhoneBill->new($file) or die "Can't parse bill";

while (my $call = $bill->next_call) { My::PhoneBill::Call->create({ number => $call->number, calldate => $call->date, calltime => $call->time, destination => $call->destination, duration => $call->duration, type => $call->type, cost => $call->cost, }); } create()调用执行SQL来为每行数据INSERT行。因为我们在使用Class::DBI而且设置了主键为AUTO_INCREMENT,我们就不需要为那个列来提供一个值。对于支持序列的数据库来说,我们也可以提醒Class::DBI需要使用哪个序列来为主键提供下一个唯一值。 现在我们已经有了一个填充了通话数据的表,接着要开始查询数据了。下面就要写个简单的脚本来报告与特定号码的通话记录。

#!/usr/bin/perl

use My::PhoneBill::Call;

my $number = shift or die "Usage: $0 ";

my @calls = My::PhoneBill::Call->search(number => $number); my $total_cost = 0;

foreach my $call (@calls) { $total_cost += $call->cost; printf "%s %s - %d secs, %.1f pence/n",

$call->calldate, $call->calltime, $call->duration, $call->cost; } printf "Total: %d calls, %d pence/n", scalar @calls, $total_cost; 这里看到Class::DBI提供了一个'search'方法给我们用。我们提供一对对的列/值的杂凑来得到所有符合条件的记录。每个记录都是Call对象的一个实例,每个实例也有对应于列名字的存取方法。(这是一个可以调整值的方法,我们可以用来修改记录,但目前我们只关心报表) 有了这个脚本后,如果我们想看看打了报时台几次,就可以运行这个命令

perl calls_to 123 2002-09-17 11:06:00 - 5 secs, 8.5 pence 2002-10-19 21:20:00 - 8 secs, 8.5 pence Total: 2 calls, 17 pence 同样的,若我们想看看某天的所有通话,就可以写个'calls_on'脚本: #!/usr/bin/perl

use My::PhoneBill::Call;

my $date = shift or die "Usage: $0 ";

my @calls = My::PhoneBill::Call->search(calldate => $date); my $total_cost = 0;

foreach my $call (@calls) { $total_cost += $call->cost; printf "%s) %s - %d secs, %.1f pence/n", $call->calltime, $call->number, $call->duration, $call->cost; } printf "Total: %d calls, %d pence/n", scalar @calls, $total_cost; 运行这个命令得到结果:

perl calls_on 2002-10-19 ... 18:36:00) 028 9024 4222 - 41 secs, 4.2 pence 21:20:00) 123 - 8 secs, 8.5 pence ... Total: 7 calls, 92 pence 就像前面保证的我们可以不用写SQL就能存取数据库。虽然还没有做什么非常复杂的事情,但是这个小例子也可以让我们的生活更加容易。 建立一个电话本 过去我总是对电话号码有很好的记性。但是诺基亚,爱立信这些公司密谋陷害我。我手机里面的内嵌电话本使我对10/11位数字的记忆能力降低了。现在我看到'calls_on'的输出的时候,已经没法知道"028 9024 4222"代表什么。现在我们需要一个存有联系人信息的电话本来解释这些报表中的数字。 第一步要做的是把我们的信息整理的更整洁点。我们将把号码和通话方两列移到"recipient"表,并增加一个名字列。"Destination"这个词也不能很好的表达和号码的关系,更不用说和通话之间的关系了,所以我们要把它改名为"location"。

CREATE TABLE recipient ( recipid MEDIUMINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, number VARCHAR(20) NOT NULL, location VARCHAR(255) NOT NULL, name VARCHAR(255), KEY (number) ); 接着我们建立表相应的类: package My::PhoneBill::Recipient; use base 'My::PhoneBill::DBI';

PACKAGE->table('recipient'); PACKAGE->columns(All => qw/recipid number location name/);

1; 还需要修改Call表的定义: CREATE TABLE call ( callid MEDIUMINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, recipient MEDIUMINT UNSIGNED NOT NULL, calldate DATE NOT NULL, calltime TIME NOT NULL, type VARCHAR(50) NOT NULL, duration SMALLINT UNSIGNED NOT NULL, cost FLOAT(8,1), KEY (recipient) ); 和相应的类: package My::PhoneBill::Call; use base 'My::PhoneBill::DBI'; PACKAGE->table('call'); PACKAGE->columns(All => qw/callid recipient calldate calltime type duration cost/); 1; 下面我们要修改填充数据库的脚本: #!/usr/bin/perl use Data::BT::PhoneBill; use My::PhoneBill::Call; use My::PhoneBill::Recipient;

my $file = shift or die "Need a phone bill file"; my $bill = Data::BT::PhoneBill->new($file) or die "Can't parse bill";

while (my $call = $bill->next_call) { my $recipient = My::PhoneBill::Recipient->find_or_create({ number => $call->number, location => $call->destination, }); My::PhoneBill::Call->create({ recipient => $recipient->id, calldate => $call->date, calltime => $call->time, duration => $call->duration, type => $call->type, cost => $call->cost, }); } 这次我们要作的变动是先建立Recipient,这样才可以从Call中建立指向它的链接。但是我们不必为每个通话建立一个新的Recipient,若我们已经打过电话给某人,那recipient表就会有一个记录。因此我们用find_or_create来取回可能已经存在的条目,或建立一个新条目。 表里面重新填充数据后,我们回到报表脚本。

我们的calls_on脚本现在会失败,因为我们现在不能直接获得通话的'number'了。下面我们得修改成:

#!/usr/bin/perl

use My::PhoneBill::Call;

my $date = shift or die "Usage: $0 ";

my @calls = My::PhoneBill::Call->search(calldate => $date); my $total_cost = 0;

foreach my $call (@calls) { $total_cost += $call->cost; printf "%s) %s - %d secs, %.1f pence/n", $call->calltime, $call->recipient, $call->duration, $call->cost; } printf "Total: %d calls, %d pence/n", scalar @calls, $total_cost; 但是这个脚本的运行结果和我们的期望不同:

perl calls_on 2002-10-19 ... 18:36:00) 67 - 41 secs, 4.2 pence 21:20:00) 47 - 8 secs, 8.5 pence ... Total: 7 calls, 92 pence 我们得到了recipient表中的ID而不是电话号码,ID不过是一个自增的值。 为了使这个值成为有意义的值,我们在Call类中增加下面的行:

PACKAGE->has_a(recipient => 'My::PhoneBill::Recipient'); 这告诉它recipient方法不是简单的输出一个值而是把那个值转化成一个Recipient类的实例。 当然calls_on还是不正确的:

perl calls_on 2002-10-19 ... 18:36:00) My::PhoneBill::Recipient=HASH(0x835b6b8) - 41 secs, 4.2 pence 21:20:00) My::PhoneBill::Recipient=HASH(0x835a210) - 8 secs, 8.5 pence ... Total: 7 calls, 92 pence 但是现在只要做个小修改就可以了: printf "%s) %s - %d secs, %.1f pence/n", $call->calltime, $call->recipient->number, $call->duration, $call->cost; 现在所有的一切又完美了: perl calls_on 2002-10-19 ... 18:36:00) 028 9024 4222 - 41 secs, 4.2 pence 21:20:00) 123 - 8 secs, 8.5 pence ... Total: 7 calls, 92 pence calls_to脚本需要更多技巧,因为搜索的开始现在是recipent而不是call。 所以我们把搜索的开始改成:

my ($recipient) = My::PhoneBill::Recipient->search(number => $number) or die "No calls to $number/n"; 然后我们需要获得打往那个recipient的所有通话。为了实现这个我们需要申明Recipient和Call之间的关系。和在Call类建立的has_a关系不同,recipient表并不存储和call表的每条通话记录相关的信息。对这种情况我们要为Recipient类添加has_many申明。 PACKAGE->has_many(calls => 'My::PhoneBill::Call'); 这就为Recipient对象实例建立了一个新的名叫calls的方法,调用它会返回用recipient外键相关的所有Call对象。 这样在calls_to脚本里面已经找到了recipient的前提下,我们只要这样:

my @calls = $recipient->calls; 这个脚本现在可以和以前一样工作了: #!/usr/bin/perl

use My::PhoneBill::Recipient;

my $number = shift or die "Usage: $0 ";

my ($recipient) = My::PhoneBill::Recipient->search(number => $number) or die "No calls to $number/n"; my @calls = $recipient->calls;

my $total_cost = 0;

foreach my $call (@calls) { $total_cost += $call->cost; printf "%s %s - %d secs, %.1f pence/n", $call->calldate, $call->calltime, $call->duration, $call->cost; } printf "Total: %d calls, %d pence/n", scalar @calls, $total_cost; 输出也是老样子:

perl calls_to 123 2002-09-17 11:06:00 - 5 secs, 8.5 pence 2002-10-19 21:20:00 - 8 secs, 8.5 pence Total: 2 calls, 17 pence 下面我们写个脚本来为地址本里面某个电话号码设置名字: #!/usr/bin/perl

use My::PhoneBill::Recipient;

my($number, $name) = @ARGV; die "Usage $0 /n" unless $number and $name;

my $recip = My::PhoneBill::Recipient->find_or_create({number => $number}); my $old_name = $recip->name; $recip->name($name); $recip->commit;

if ($old_name) { print "OK. $number changed from $old_name to $name/n"; } else { print "OK. $number is $name/n"; } 这使我们可以建立数字和名字间的关联:

perl add_phone_number 123 "Speaking Clock" OK. 123 is Speaking Clock 现在只要很小的改动就可以使calls_on脚本输出我们熟知的名字: printf "%s) %s - %d secs, %.1f pence/n", $call->calltime, $call->recipient->name || $call->recipient->number, $call->duration, $call->cost; perl calls_on 2002-10-19 ... 18:36:00) 028 9024 4222 - 41 secs, 4.2 pence 21:20:00) Speaking Clock - 8 secs, 8.5 pence ... Total: 7 calls, 92 pence 要让calls_to脚本能同时接受名字或号码参数,我们可以这样: my $recipient = My::PhoneBill::Recipient->search(name => $number) || My::PhoneBill::Recipient->search(number => $number) || die "No calls to $number/n"; 然而,一个名字可能对应多个号码。因为我们在标量环境而不是列表环境里调用search方法,我们会得到一个iterator而不是每个Recipient对象。我们需要遍历每个对象来完成工作: my @calls; while (my $recip = $recipient->next) { push @calls, $recip->calls; } (打印每个号码的小计功能留给读者作练习。) perl calls_to "Speaking Clock" 2002-09-17 11:06:00 - 5 secs, 8.5 pence 2002-10-19 21:20:00 - 8 secs, 8.5 pence Total: 2 calls, 17 pence 和其他模块协作 有时我们需要在数据库里存储其他模块需要使用的数据。比如我们通话需要有个不同的日期类型,我们更喜欢使用Date::Simple类型的对象。Class::DBI也使这个目标易于实现。 我们还是在Call类里用has_a来申明这个关系:

PACKAGE->has_a(recipient => 'My::PhoneBill::Recipient'); PACKAGE->has_a(calldate => 'Date::Simple'); 这样我们获取calldate的时候它就自动被展开成Date::Simple对象。这样我们就可以为calls_to的输出设计一个更漂亮的格式: printf "%s %s - %d secs, %.1f pence/n", $call->calldate->format("%d %b"), $call->calltime, $call->duration, $call->cost;

perl calls_to "Speaking Clock" 17 Sep 11:06:00 - 5 secs, 8.5 pence 19 Oct 21:20:00 - 8 secs, 8.5 pence Total: 2 calls, 17 pence Class::DBI假定任何非Class::DBI类是通过new方法展开,通过stringification来压缩。因为Date::Simple确实支持这个,我们就不需要再做更多了。若不是这样,例如你想用Time::Piece类而不是Date::Simple类,你就要告诉Class::DBI如何在内存值和数据库之间进行数据展开和压缩。 PACKAGE->has_a(calldate => 'Time::Piece', inflate => sub { Time::Piece->strptime(shift, "%Y-%m-%d") }, deflate => 'ymd' ); 将Time::Piece对象压缩成适合MySQL的ISO日期类型非常容易:你只要调用类的ymd()方法就好了。这样我们就把它序列化成一个字符串。解压就麻烦了,这需要调用一个带两个参数的strptime()方法。这样我们必须使用一个函数引用。这样我们可以告诉strptime用什么格式来分析日期字符串。 用Time::Piece而不是Date::Time需要我们对输出部分的代码如下更改:

printf "%s %s - %d secs, %.1f pence/n", $call->calldate->strftime("%d %b"), $call->calltime, $call->duration, $call->cost; 最常用号码 BT给我们一个设定10个最常用号码并节省通话费的服务。这就使我们有必要看看那些号码上花了最多的钱。我们假定那些只打过一次的电话号码没必要分析。我们只关心头十个不只一次通话的花费最多的号码。 前面讲过,Class::DBI不是试图用语法来表达任何SQL,因此有的数据没法直接获得。我们还是用最简单的方法。

首先我们为Recipient类增加一个方法来告诉我们与这人通话我们花了多少:

use List::Util 'sum';

sub total_spend { my $self = shift; return sum map $_->cost, $self->calls; } 然后我们就可以写一个top_ten脚本了: #!/usr/bin/perl

use My::PhoneBill::Recipient;

my @recipients = My::PhoneBill::Recipient->retrieve_all; my @regulars = grep $_->calls > 1, @recipients; my @sorted = sort { $b->total_spend <=> $a->total_spend } @regulars;

foreach my $recip (@sorted[0 .. 9]) { printf "%s - %d calls = %d pence/n", $recip->name || $recip->number, scalar $recip->calls, $recip->total_spend; } 这是很慢的方法,尤其是数据库里有百十个以上的通话记录的时候。主要的开销在于我们总是用方法的返回值来作排序的比较值。用Schwartzian Transform来替换排序会显著的提高性能: my @sorted = map $->[0], sort { $b->[1] <=> $a->[1] } map [ $, $_->total_spend ], @regulars; 在数据库内容显著的增多以前,这个方法就快多了,尤其是你不经常运行这个脚本的情况下。 但是这还不够,我们可以直接用SQL。理所应当的,在为速度优化时你会损失其他的性能,在这个例子里面损失可能是可移植性。现在我们的例子用MySQL,我们会在Recipient.pm里面增加MySQL才支持的查询:

PACKAGE->set_sql(top_ten => qq{ SELECT recipient.recipid, SUM(call.cost) AS spend, COUNT(call.callid) AS calls FROM recipient, call WHERE recipient.recipid = call.recipient GROUP BY recipient.recipid HAVING calls > 1 ORDER BY spend DESC LIMIT 10 }); 接着我们可以建立一个返回很多对象的方法: sub top_ten { my $class = shift; my $sth = $class->sql_top_ten; $sth->execute; return $class->sth_to_objects($sth); } 任何用set_sql设定的SQL都可以用sql_取出来成为一个编译好代执行的DBI语句句柄。所以我们用my $sth = $class->sql_top_ten来取回top_ten。 我们可以就这么干并调用那些传统的DBI命令如fetchrow_array等,也可以更进一步偷懒。既然我们的查询输出的列中有一个是Recipient的主键,我们就可以把查询结果喂给sth_to_objects,这个Class::DBI的底层方法使得查询可以返回对象实例列表。

这样我们的脚本就变的简单了:

foreach my $recip (My::PhoneBill::Recipient->top_ten) { printf "%s - %d calls = %d pence/n", $recip->name || $recip->number, scalar $recip->calls, $recip->total_spend; } 如上所示,Class::DBI使得通常的数据库编程变得不值一提(不用写一行SQL代码)。但在你真的需要的时候,也可以很容易的编写你需要的SQL并执行。

转载于:https://my.oschina.net/OliverTwist/blog/213561

Class::DBI模块简介相关推荐

  1. collections模块简介

    collections模块简介 除python提供的内置数据类型(int.float.str.list.tuple.dict)外,collections模块还提供了其他数据类型,使用如下功能需先导入c ...

  2. 【STM32】ESP8266模块简介

    00. 目录 文章目录 00. 目录 01. ESP8266模块简介 02. 特性参数 03. 模块引脚 04. TK-ESP-01 WIFI模块 05. 模块说明 06. 附录 07. 声明 01. ...

  3. 模块简介与matplotlib基础

    模块简介与matplotlib基础 1.基本概念 1.1数据分析 对已知的数据进行分析,提取出一些有价值的信息. 1.2数据挖掘 对大量的数据进行分析与挖掘,得到一些未知的,有价值的信息. 1.3数据 ...

  4. configparser模块简介

    目录 configparser模块简介 看一下configparser生成的配置文件的格式 现在看一下类似上方的配置文件是如何生成的 读取文件内容 修改文件内容 configparser模块简介 该模 ...

  5. Aurix TC3xx系列MCU ADC模块简介(一)

    文章目录 1 前言 2 ADC模块简介(TC3xx) 1.1 ADC模块特点 1.2 转换器内部结构 1.3 转换时间 3 EDSADC模块简单介绍 >>返回总目录<< 1 前 ...

  6. IoT物联网嵌入式设备中30种常见传感器模块简介及原理讲解

    IoT物联网嵌入式设备中30种常见传感器模块简介及原理讲解 0.前言 一.光学传感器模块: 1. 光敏传感器模块: 2. 红外避障模块 3. 循迹传感器模块 4. U型光电传感器模块 5. 红外接收模 ...

  7. Python常用模块4-Python的datetime及time模块简介

    文章目录 一.Python datetime模块介绍 1.1 有效的类型 1.2 timedelta 类对象 1.2.1 timedelta.total_seconds()方法 1.3 date对象 ...

  8. Python_pygame库学习笔记(1):pygame的由来,特点以及模块简介

    Python_pygame库学习笔记 1 Pygame库的由来: Python适合用来开发游戏吗? Pygame的安装 Pygame模块简介 Pygame库的由来: 2000年,作者Pete Shin ...

  9. Schrodinger 功能模块简介

    Schrödinger(薛定谔)是药物发现的完整软件包,包括:基于受体和配体结构的诱导契合和柔性对接模式:基于受体结构及配体极性的对接模式:基于受体结构及溶液环境性质的对接模式:组合化学库设计及基于组 ...

最新文章

  1. javascript页面登录代码_自己动手做一个很酷的登录页面
  2. HTML5 Web SQL数据库
  3. java笔记15-日期类
  4. Java程序安装失败
  5. linux svn权限如何打开文件,如何让 SVN 或者 GIT 保留 Linux 文件权限
  6. 数组——寄包柜(洛谷 P3613)
  7. 图片 和 base64 互转
  8. Umbraco中的Examine Search功能讲解
  9. 从本科到研究生,看大疆工程师给你定制的机器人学习计划
  10. 华为java安全编程规范考试答案
  11. 计算机test的应用,例举内存检测工具memtest详细使用教程
  12. Flutter支付宝授权登录
  13. 马斯克回应福特CEO喊话:我已经有一辆Cybertruck了
  14. 微服务并不能修复你破碎的组织文化
  15. AWVS批量扫描-妈妈再也不用担心我不会用awvs批量扫描了
  16. 我在国图读完的第二本书 —— 《经济学的思维方式》
  17. NCBI中SRA数据下载
  18. QQ空间背景音乐 链接制作
  19. .Net框架中的CLR,CTS,ClS的解释
  20. 讲一个我个人的故事!如何活到今天

热门文章

  1. 互联网应用 zzl复习版
  2. Oracle 三种常与开窗组合使用的方法
  3. SNS2124(OEM博科FC交换机)忘记密码,密码初始化
  4. 上传码云遇到git did not exit cleanly 的问题
  5. 串口通信(串口助手发送数据给单片机,单片机原封不动发给串口助手)
  6. 计算机硬件发展慢,老电脑卡慢应该更换哪些硬件?看完秒懂
  7. 回炉夜话 - HTML5
  8. oneinstack申请免费的R3 域名证书
  9. 笔记-中项/高项学习期间的错题笔记1
  10. PCB吉米哥:如何阅读电路原理图及PCB设计