DBI 数据库模块剖析
为了和数据库进行通讯Perl 社区开发出了统数据库通讯接口模块:DBIDBI 作为 Perl 语言中和数据库进行通讯标准接口它定义了系列思路方法变量和常量成功地提供个和具体数据库平台无关数据库持久层
DBI 模块体系结构
概述
整个 DBI 模块结构可以被分成两个主要部分:DBI 模块本身和实现和具体数据库平台通讯驱动模块DBI 模块用于定义提供给 Perl 开发者使用编程接口和对区别数据库驱动模块思路方法具体数据库通讯驱动模块实现和特定数据库平台有关并且负责和具体数据通讯实际操作
DBI 模块中 3种句柄
在 DBI 模块定义中用户可以创建 3种区别类型句柄
驱动模块句柄(Driver Handle):驱动模块句柄代表个在内存中加载驱动它在 DBI 模块加载其对应驱动模块被创建它和实际驱动模块的间是对应关系驱动模块句柄提供了两个主要思路方法是 data_sources 和 connect
数据库句柄(Database Handles):数据库句柄是员使用 DBI 模块和后台数据库进行通讯第步它包含了个到特定数据库某个独立连接表给出了对于常见数据库连接串:
执行语句句柄(Statement Handles):执行语句句柄在 DBI 规范标准中被定义为和数据库进行交互和操作接口这些句柄包装了条条 SQL 语句并将它们交送给后台数据库执行个使用执行语句句柄执行 SQL 语句例子见
表 1. 常见数据库连接串
数据库 连接串(举例)
DBD::mysql “DBI:mysql:database=$dbname;host=$hostname;port=$port”;
DBD::Oracle “dbi:Oracle:$dbname”; “dbi:Oracle:host=$hostname;sid=$sid”;
DBD::DB2 “dbi:DB2:$dbname”;
DBD::Sybase “dbi:Sybase:host=$hostname;database=$dbname;port=$port”;
清单 1. 使用 DBI 模块连接数据库并执行 SQL 语句
$dbh = DBI->connect ($connection_, $userid, $passwd);
$sth = $dbh->prepare (“SELECT * FROM tbl”);
$sth->execute;
while (@row = $sth->fetchrow_.gif' />) {
pr “Each record: @row \n”;
}
$sth->finish;
$dbh->disconnect;
建立和释放连接
使用 DBI 模块提供 connect()思路方法创建个数据库句柄是员必须提供个数据源用于指定要连接数据库DBI 模块规范标准要求数据源名字并须以 dbi: 开头然后接上数据库通讯驱动模块名字并且以‘ : ’结尾比如‘ dbi:db2: ’和 connect()思路方法相对应DBI 模块中还定义了个 disconnect()思路方法
通过 DBI->available_drivers()思路方法员可以得到已安装在当前机器上所有数据库通讯驱动模块列表接着用驱动模块名作为参数 DBI->data_sources()思路方法可以得到对应此驱动模块所有数据源列表 2给出了个这样例子 .
清单 2. 得到所有支持驱动模块所有数据源
my @drivers = DBI->available_drivers;
die “No dirvers d! \n” unless @drivers;
foreach my $driver (@drivers) {
pr “Driver: $driver \n”;
my @data_sources = DBI->data_sources ($driver) {
foreach my $data_source (@data_sources) {
pr “\t Data Source: $data_source \n”;
}
pr “\n”;
}
处理
在 DBI 模块中提供了两种处理思路方法第种思路方法依靠员手工检测被思路方法返回值;第 2种思路方法通过 DBI 模块对进行自动检测这种思路方法类似于异常处理机制对于手工处理可以通过将“PrError”和“RaiseError”两个变量设定为 0 来激活在默认情况下“PrError”参数是被激活
思路方法在 DBI connect()参数中设置:
%attr = (PrError => 0, RaiseError=>0);
my $dbh = DBI->connect (“dbi:Oracle:testdb”, “username”, “password”, \%attr);
思路方法 2在数据库句柄中直接设置:
$dbh->{PrError} = 0; $dbh->{RaiseError} = 0;
对于自动检测DBI 提供了两种区别级别处理思路方法用于句柄“PrError”参数在被设置为 1 时候DBI 模块会 warn()进行处理它会将信息打印到屏幕但是并不会中止进程而用于句柄“RaiseError”参数在被设置为 1 时候DBI 模块会 die()并且中止进程这两个区别级别处理思路方法可以在 DBI 模块任何个有效句柄中被激活
除了处理手段以外DBI 模块还提供了对信息进行诊断思路方法这些思路方法可以对于任何个有效句柄进行使用它们返回值包括号和信息
$rv = $h->err; $str = $h->errstr; $str = $h->state;
$h->err()思路方法返回个由底层数据库生成号;$h->errstr()思路方法则返回个由底层数据库生成信息描述;$h->state()思路方法返回个 5 位 SQLSTATE 串除了上面 3种思路方法会返回信息供排错的外在 DBI 模块级别$DBI::err$DBI::errstr$DBI::state 会返回和上述同样值个利用 DBI 模块内置处理思路方法例子见 3
清单 3. 利用 DBI 模块内置处理思路方法
while (1) {
my $dbh;
# disable automatic error handle
until {
$dbh = DBI->connect ($connection_, $userid, $username);
$dbh->{PrError} = 0;
$dbh->{RaiseError} = 0;
warn “Unable to connect: $DBI::errstr. sleep for 5 minutes. \n”;
sleep (300);
}
# enable automatic error handle
eval {
$dbh->{RaiseError} = 1;
my $sth = $dbh->prepare (“SELECT foo, bar from tbl”);
while (1) {
$sth->execute;
while ( my @row = $sth->fetchrow_.gif' />) {
pr “Row: @row \n”;
}
sleep 60;
}
}
warn “Monitoring aborted by error: $@\n” $@;
sleep 5;
}
操作数据库
执行简单查询
应用和数据库的间最常见操作是从数据库中查询并且提取数据在标准 SQL 语句规范标准中这过程是使用关键字 SELECT
个通过 DBI 执行标准简单查询包括以下 4个阶段:
准备阶段(Perpare SQL statement):
通过 prepare()思路方法准备阶段解析 SQL 语句对 SQL 语句进行验证并且返回个执行语句句柄这个句柄代表将在数据库内被执行这条 SQL 语句
执行阶段(Execute select statement):
通过 execute()思路方法执行阶段执行 SQL 语句查询数据库并且以被查询数据填充 Perl 数据结构但是在这阶段中你 Perl 应用并未真正地访问到被查询数据
数据抽取阶段(fetching date):
第 3阶段被称为数据抽取阶段在这阶段实际数据从数据库中被抽取出来通过 fetch()思路方法族组思路方法数据抽取阶段从数据库得到查询所得数据以每条数据为单位注入 Perl 数据结构DBI 提供了多种思路方法对数据进行抽取可以将被抽取数据用个列表个指向引用或是个指向哈希表引用方式提供给应用而且每条记录中字段顺序也是由 SQL 语句中指定顺序所决定个包含 3种思路方法例子见 4
完成阶段(finishing date fetch):
最后个阶段被称为完成阶段这个阶段主要释放资源并且清理相关数据结构中保存历史信息通过显式地 finish()思路方法来完成当个执行语句句柄(statement handler)被成功执行后它状态会被标记成为活跃你可以通过访问执行语句句柄 Active 属性来访问它在用户执行 fetch()思路方法从数据库中抽取了最后列数据的后数据库驱动自动关闭数据库中正在进行地和这个执行语句句柄有关工作并且重置 Active 属性为不活跃状态这切工作都是在读取了最后列数据的后被自动触发在大多数情况下用户并不需要额外地关心这过程中后台所作工作某些额外情况需要应用主动 finish()思路方法释放资源个典型例子就是:当数据库占用了数目可观磁盘空间存储临时文件存放查询结果而应用又不需要保存所有查询结果比如执行条形如 “SELECT EMP_DEP, count(*) FROM EMP GROUP BY EMP_DEP ORDER BY count(*) DESC” SQL 语句在应用只需要部分统计结果情况应该显示 finish()思路方法释放被申请和占用机器资源
清单 4. 3种区别数据抽取思路方法
$sth->execute;
# fetch data by an .gif' />
while ( @row = $sth->fetchrow_.gif' /> ) {
pr “Column1: $row[0] \t Column2: $row[1]. \n”;
}
# fetch data by a reference poing to .gif' />
while ( $.gif' />_ref = $sth->fetchrow_.gif' />ref ) {
pr “Column1: $.gif' />_ref->[0] \t Column2: $.gif' />_ref->[1]. \n”;
}
# fetch data by a reference poing to hash table
while ( $hash_ref = $sth->fetchrow_hashref ) {
pr “Column1: $hash_ref->{column1} \t Column2: $hash_ref->{column2}. \n”;
}
执行非查询语句
在数据库常用 DML 语句中除了 SELECT 语句的外还有 INSERTDELETEUPDATE 3种我们统称这 3种语句为非查询语句和查询语句 SELECT 区别它们只是改变了数据库中部分纪录而不会返回个记录集给应用所以相对对于查询语句中 prepare-execute-fetch-deallocate 序列来说非查询语句不需要数据抽取阶段同时也可以将 prepare 和 execute 阶段用个 do()思路方法来完成个 do()思路方法例子如下 :
$affected_row_number = $dbh->do (“DELETE FROM tbl WHERE foo = ‘ bar ’”);
DBI 模块提供了个 do()思路方法用以简化工作取代了原先需要被 prepare()和 execute()思路方法实际上DBI 模块中 do()思路方法只是简单地包含了 prepare()和 execute()思路方法这种思路方法在使用添写思路方法生成 SQL 执行语句时候和分别使用 prepare()思路方法和 execute()思路方法没有任何性能上面区别但是如果使用了参数绑定 SQL 执行语句生成思路方法两者在性能上将会有显著地差别使用 do()思路方法话以 INSERT 操作为例对于每条被插入记录数据库必须解析每条插入语句并且生成执行计划然而使用 prepare()思路方法话可以在 prepare()思路方法中使用占位符而使得所有插入语句可以共用个执行计划达到了提升效率目地具体例子见 6
参数绑定
在准备阶段有个重要概念就是数据绑定和的相关有 3个术语:占位符(placeholder)参数(parameter)和绑定(binding)他们都是用来根据上下文动态地生成 SQL 语句将变量放入 SQL 语句思路方法有两种:第种通过添写思路方法生成 SQL 语句(erpolated SQL statement creation)该思路方法将变量直接通过串和 SQL 语句其它部分连接起来生成可以被执行 SQL 语句 5展示了个利用 Perl 串技术生成个 SQL 执行语句
清单 5. 使用 Perl 串技术动态生成 SQL 执行语句例子
foreach $table_name ( qw (table1, table2, table3) ) {
$sth = $dbh->prepare(“SELECT count(*) FROM $table_name”);
$sth->execute;
}
第 2种思路方法在 SQL 语句中使用占位符并且通过 bind_param()思路方法将变量和的绑定生成 SQL 语句无论何时bind_param()思路方法必须在 execute()思路方法的前被否则被绑定参数无法填入 SQL 语句中而对 SQL 语句也注定会失败采用绑定思路方法是成 SQL 语句(Bind value SQL statement creation)个典型例子见 6
清单 6. 使用占位符动态生成 SQL 执行语句例子
$sth = $dbh->prepare (“SELECT foo, bar FROM table WHERE foo = ? AND bar = ?”);
$sth->bind_param (1, “FOO”);
$sth->bind_param (2, “BAR”);
$sth->execute;
第种思路方法使用 Perl 串处理生成条完整 SQL 语句并将它送往后台数据库;而绑定思路方法则区别它将含有占位符 SQL 语句和绑定值分开传送给数据在数据后台处理数据绑定然后执行绑定后 SQL 语句两种区别处理思路方法将会带来性能上巨大差异尤其是在有大量相似 SQL 语句需要被处理情况下主流大型数据库都有种被称为“Shared SQL Cache”部件它存储了诸如查询语句执行计划的类辅助数据结构帮助数据库执行 SQL 语句在得到个新处理 SQL 语句请求时如果 SQL 语句已经存在于“Shared SQL Cache”中数据库就不需要重新处理这条语句而可以重用 Cache 中存储信息这样就可以带来性能上显著提升
存储过程
存储过程运行在后台数据库上可以有效地减少客户端和数据库的间通讯量在这种工作模式下客户端不再需要将每条需要被执行 SQL 语句发往后台数据库借助存储过程可以将所有要执行 SQL 语句定义在个存储过程内统执行并且返回运算结果给客户端
在 DBI 模块中有个和 bind_param()思路方法相似思路方法叫做 bind_param_inout()思路方法可以从执行语句句柄中直接返回值这个思路方法最主要应用在于存储过程用来接收参数和返回结果需要注意是某些数据库(比如 MySql)不支持这种思路方法bind_param_inout()思路方法通过传递引用方式向数据库传入个可以接受返回值参数个简单例子如下:
$sth->bind_param_inout (1, \$bar, 50);
上面例子当中bind_param_inout()思路方法第 3个参数是返回值最大长度
个在 DB2 数据库中存储过程完整举例见 7
清单 7. DB2 数据库中存储过程例子
create procedure proc (in foo char(6), out bar eger)
specic proc_example
dynamic result s 0
modies sql data
not deterministic
language sql
begin atomic
insert o tbl ( ‘ foo ’ , current date);
select count(*) o bar from tbl where foo = ‘ foo ’ ;
end
# start perl script to call sql procedure
$sth = $dbh->prepare (“CALL proc (?,?)”);
$sth->bind_param (1, $foo);
$sth->bind_param_inout (2, \$bar, 50);
$sth->execute;
pr “stored procedure ed $bar. \n”;
执行数据库事务
数据库事务是种将组相互的间有密切关系 SQL 语句放到起执行技术它们要么都被成功执行要么都执行失败我们称的为“all-or-nothing”模式个事务从它第条可被执行 SQL 语句开始到被提交(commit)或者回滚(rollback)结束如果个事务被提交那么它对数据库所作所有修改都会被保存并且对其它并发过程可见;如果个事务被回滚它对数据库所作所有修改都会被放弃
并不是所有数据库软件Software都支持事务但是对于所有支持事务数据库DBI 模块提供了统接口用于操作事务尽管数据库实现各不相同DBI 模块提供了事务自动提交(auutomatic transcation committing)和手工事务处理(powerful manual transaction)两种区别处理方式如果用户在创建个数据库句柄(database handle)时将句柄参数“AutoCommit”设置为 1那么通过这个数据库句柄执行每条 SQL 语句操作都会被立即提交而不需要任何显式语句进行提交或回滚;反的如果参数“AutoCommit”被设置为 0则每个事物都必须以显示地 commit()思路方法或 rollback()思路方法来结束如果后台数据库不支持事务处理那么当用户试图修改“AutoCommit”参数为 0 时候DBI 模块将会抛出在 DBI 模块中定义了 commit()思路方法用来显式地提交在个事务范围内对数据库所作修改该思路方法通过数据库句柄来如下:
$dbh->commit;
如果 commit()思路方法数据库句柄中“AutoCommit”属性被设置为 1那么 commit()思路方法的后会得到个“commit ineffective with AutoCommit”警告信息如果后台数据库不支持事务处理那么“AutoCommit”属性默认打开每次 commit()思路方法的后也会得到个相同警告信息和的相似是 rollback()思路方法每次 rollback()思路方法会回滚事务范围内对数据库所作所有修改如果“AutoCommit”设置为或者是后台数据库不支持事务处理同样警告信息也将会被返回个 rollback()思路方法例子如下:
$dbh->rollback;
不幸事情是当“AutoCommit”属性被设置为 0 时候显式地 disconnect()思路方法终止和数据库连接后触发行为无法预测在某些数据库版本中在 disconnect()思路方法被 DBI 模块的前会自动 commit()思路方法提交所有对数据库修改;也有某些数据库版本会 rollback()思路方法回滚所有对数据库所作修改个综合举例见 8
清单 8. DBI 模块处理数据库事务详细例子
$dbh->connect;
$dbh->{AutoCommit} = 0;
$dbh->{RaiseError} = 1;
eval {
load_some_data_to_database;
insert_some_data_to_database;
delete_some_data_from_database;
$dbh->commit
}
($@) {
$dbh->rollback;
}
$dbh->disconnect;
DBI 模块中高级主题
DBI 中句柄属性
除了和数据库句柄和执行语句句柄绑定组思路方法以外DBI 模块也提供了组和这些句柄有关属性供用户对他们执行环境进行调优这些句柄属性实质是个由“键 / 值”对组成哈希表可以通过和操作哈希表引用样思路方法来访问和修改这些属性个典型例子见 9
清单 9. 修改和显示句柄属性
$dbh->connect($connect_, $userid, $passwd);
$dbh->{AutoCommit} = 1;
pr “AutoCommit: $dbh->{AutoCommit} \n”;
当用户访问或者设置这些句柄属性时DBI 模块自动检查用户输入属性名称如果用户试图引用个位置属性时DBI 模块将会产生个同理如果用户试图修改个只读属性时DBI 模块会以同样方式抛出个无论用户有没有在创建数据库句柄时设置了“RaiseError”属性在检测到上述的后DBI 模块都会使用 die()思路方法抛出返回个值
句柄属性命名规则
句柄属性命名看似杂乱无章其实微言大义在 DBI 模块兼容性问题发挥了重要作用句柄属性大小写命名规则直接反映了是谁定义了这个属性并对其赋值其规则有 3:
全大写命名方式(UPPER_CASE):全大写属性通常是由外部标准所定义比如说 ISO SQL 或者 ODBC
混合命名方式(Mixed_Case):此类属性名字通常由大写字母开头但是其中也混有小写字母这类属性通常由 DBI 模块标准自身定义
全小写命名方式(lower_):此类属性由各自数据库驱动所定义被称为“驱动相关”属性
公共句柄属性
表 2列出了部分最常见数据库句柄(database handle)和执行语句句柄(statement handle)都支持句柄属性
表 2. 常见公共句柄属性
属性名 介绍说明
PrError 如果此值被设置为 1当 DBI 模块返回个结果时候使用 warn()思路方法;
RaiseError 如果此值被设置为 1当 DBI 模块个结果时候使用 die()思路方法;
数据库句柄属性和数据库元数据
表 3列出了最常见数据库句柄所支持句柄属性
表 3. 常见数据库句柄属性
属性名 介绍说明
AutoCommit 如果设置为 1每条通过 DBI 模块执行结果都会被立即提交;反的如果设置为 0
所有执行结果都要用 commit()思路方法显示提交或用 rollback()思路方法回滚;
Name 只读属性存储数据库名字;
数据库原数据(database metadata)被称为“描述数据数据”用来描述数据本身当用户有动态生成 SQL 执行语句或者创建动态视图需求时候数据库元数据所提供信息显得格外地重要不用数据库厂商存储数据库元数据思路方法和保存数据库元数据各不相同大多数主流数据提供了个系统目录(system catalog)通过表和视图保存这些信息尽管上述种种差异使得 DBI 模块提供个统访问数据库元数据接口成了件任重道远任务在 DBI 模块定义中依然提供了两个可供思路方法用来访问数据库元数据
第个思路方法 $dbh->tables()返回个包含它数据库句柄(database handle)所能发现所有表和视图见十
清单 10. 利用数据库句柄返回数据库元数据
my @tables = $dbh->tables;
foreach my $table (@tables) {
pr “Table Name: ” . $table . “ \n”;
}
第 2个思路方法 $dbh->table_info()返回个包含更多详细信息执行语句句柄(statement handle)可以通过和访问普通执行语句句柄样思路方法抽取其中信息
执行语句句柄属性和表原数据
同数据库句柄样执行语句句柄(statement handle)也有自己句柄属性执行语句句柄从创建它数据库句柄继承了部分属性并且它大部分属性是用来表示语句执行状态和结果属于只读属性表 4列出了常见执行语句属性:
表 4. 常见执行语句句柄属性
属性名 介绍说明
NUM_OF_FIELDS 被 SELECT 语句返回字段数量;
NAME 被 SELECT 语句返回字段名称;
NULLABLE 某个字段是否可以为空;
TYPE 某个字段类型;
NUM_OF_PARAMS 个 SQL 执行语句所使用占位符数量;
通过读取某些执行语句句柄属性可以得到表原数据举例如下:
清单 11. 利用执行语句句柄返回表元数据
my $sth = $dbh->prepare (“SELECT * FROM tbl”);
$sth->execute;
my $field_number = $sth->{NUM_OF_FIELDS};
pr “NUMBER OF FIELDS: $field_number \n”;
pr “Column Name Type Nullable? \n”;
pr “--------------------------------------- ---- --------- \n”;
for (my $index=0; $index<$field_number; $index) {
my $name = $sth->{NAME}->[$index];
my $type = $sth->{TYPE}->[$index];
my $nullable = (“No”, “Yes”, “Unknown”)[$sth->{NULLABLE}->[$index]];
prf “%-30s %4d $s \n”, $name, $type, $nullable;
}
$sth->finish;
符合 DBI 模块接口 DBD 模块
DBI 模块定义了组和用户交互接口而 DBD 实现和数据库的间具体接口两者的间关系可以类比为面向对象编程中接口和继承接口类的间关系有两种区别数据库驱动实现方式第种是基于纯粹 Perl 语言不依赖任何 C 语言编译器这种思路方法实现最为简单却不被大多数数据库所支持比较典型 DBD 模块例子有 DBD::File 和 DBD::CSV另种思路方法更为普遍使用了 C 帮助和数据库进行通讯被称为 C/XS 驱动
对于纯粹 Perl 语言开发数据库驱动而言整个 DBD 模块核心在个 DBD::Driver.pm 模块上根据实际要连接数据库区别Driver 名字可以被替换成为 DBD::Oracle 或者 DBD::DB2在 DBD::Driver 包中通常包含子包区别子包有着区别作用有关它们详细信息见表 5
表 5. 典型 DBD 模块内部设计
包名 作用 表(部分)
DBD::Driver 提供 DBD 级别思路方法 driver()
DBD::Driver::dr 提供驱动模块句柄级别思路方法 connect()
data_sources()
DBD::Driver::db 提供数据库句柄级别思路方法 do()
prepare()
table_info()
ping()
rollback
commit
FETCH
STORE
DBD::Driver::st 提供执行语句句柄级别思路方法 execute
bind_param
fetchrow_.gif' />
fetchrow_.gif' />ref
fetchrow_hashref
基于 C/XS 驱动 DBD 模块十分复杂在这里不作介绍
基于 MySql 数据库例子
我们对 MySql 数据库进行了简单测试测试结果见表 6
表 6. MySql 数据库对 DBI 模块测试结果
测试用例 目 问题
test_dbh_and_sth 测试数据库句柄和执行语句句柄;
查询插入和删除数据库记录; 字段名称要全部小写;
test_fetch_metadata_of_table() 测试获取数据标原数据; 部分信息;
test_call_sql_procedure 测试 SQL 存储过程; bind_param_inout()无法正常工作要得到存储过程返回值需要特殊处理;
基于 DB2 数据库例子
同样我们也有个 SQL 脚本用于在 DB2 数据创建测试环境(见附件 3)测试结果见表 6
使用 DBI 模块连接 DB2 数据库举例见十 2
表 7. DB2 数据库对 DBI 模块测试结果
测试用例 目 问题
test_drh 测试驱动句柄; N/A
test_dbh_and_sth 测试数据库句柄和执行语句句柄;
查询插入和删除数据库记录; 字段名称要全部大写;
test_mixed_error_checing 测试 DBI 模块处理机制; N/A
test_bind_parameter_to_statement 测试执行语句后期绑定功能; N/A
test_fetch_metadata_of_table() 测试获取数据标原数据; 部分信息;
test_call_sql_procedure 测试 SQL 存储过程; N/A
test_run_transaction 测试通过 DBI 模块执行事务; N/A
清单 12. 使用 DBI 模块连接 DB2 数据库
sub up_connections {
my $connections_reference = sht;
my $passwd = "xxxxxxxx";
my $db2_connection_ = "dbi:DB2:test";
my $db2_userid = "db2inst1";
my %db2_connection = ;
$db2_connection{dbName} = "DB2";
$db2_connection{connStr} = $db2_connection_;
$db2_connection{userid} = $db2_userid;
$db2_connection{passwd} = $passwd;
push (@{$connections_reference}, \%db2_connection);
}
整理总结
本文详细地介绍了 Perl 语言中用于和数据库通讯 DBI 模块以细腻笔法和生动地举例给读者讲述了 DBI 模块主要组成部分结构和供用户编程时思路方法同时本文也涉及了些 DBI 模块中高级主题如对存储过程对并发事务处理和数据库及执行语句句柄属性设置和元数据处理除此的外本文还对 DBD 模块接口给予了入门级介绍希望可以对读者在专研第 3方数据库 DBD 模块时可以有所帮助在本文最后通过定义了套通用 DBI 模块测试接口对 3种主流数据库 DB2Sybase 和 MySql 进行测试通过比较测试结果得出了 DBI 模块在这 3种数据库上异同点和支持上盲点
最新评论