数据|数据库
Write once - run anywhere
一次编写——随处运行
这是Java的一句行销口号,但是它同时也是PHP的关键特性之一。许多商业模型依赖于操作系统无关性来保证产品能够销售给广泛的客户群体。因而,为什么要把你自己绑在某种数据库厂商的身上呢?数据库抽象层使得你能够与数据库独立的开发你的应用程序。但是,通常情况下它们对性能的影响超过了你所希望的,要么他们并不足够抽象以消除所有和特定数据库相关的代码。
这篇文章将教给我什么?
这篇文章将对数据库抽象包 PEAR MDB 有一个很好的介绍。文章的焦点将是对 MDB 超越类似包所提供的更先进的特性,例如数据类型抽象和基于 XML 的 schema 管理。对 PHP 和 SQL 的基本理解是推荐的。
为什么另外再要一个数据库类?
通常, web 工程在客户已经确定了要使用那种 RDBMS (关系型数据库管理系统)之后被添加给已经存在的 IT 基础结构。即使那并不是因为不同的预算可能影响的你选择何种数据用于部署的情况。最终,你作为开发者可能简单的偏好于不把自己绑在某个厂商身上。自此,意味着给每个支持的数据保持版本或者牺牲更多性能但是获得多于必须的易用性:走入 PEAR MDB 吧。
MDB 是着眼于使得编写 RDBMS 无关的 PHP 程序成为简单的过程的数据库抽象层。大部分其他的 PHP 的所谓数据库抽象层紧紧给所有支持的数据库提供了一个公用 API 以及非常有限的抽象(大部分只是针对序列的)。MDB 另一方面能够用来抽象所有数据库发送和接收的数据。甚至数据库 schema 都能被定义为 RDBMS 无关的格式。但是它提供这些功能的同时仍然保持了很高的性能以及简单易用。这是通过深入观察两个流行的数据库抽象层,PEAR DB 和 Metabase, 之后并且对它们进行了融合后获得的。而且在融合过程中,趁着这个机会清理了它们融合后的 API 以及任何影响性能的设计。
MDB 是怎样出现的?
早在 2001 年的秋天,我就在寻找一种可能能够让我公司的程序框架与 RDBMS 独立的数据库抽象包。这个目标是把特定数据库相关的代码数量减少到零。我发现提供这样的功能的唯一的一个包是 Metabase。但是 Metabase有一些部分是因为为了和 PHP3 兼容的让人不舒服的 API。尽管如此,我们决定 Metabase 是我们唯一的选择。但是即使是在给 Metabase 增加了一个性能改进的补丁之后,我们仍然感到我们放弃了太多的性能。我们在 2001 年的 PHP 国际会议上碰到了 Metabase 的作者,并且我们谈论了让像 Metabase 这样的东西成为 PEAR 工程一部分的好处。后来不久,在 PEAR 邮件列表上就 PEAR DB 和 Metabase 融合的可能的好处又开始了一场讨论。在我们公司进行了许多讨论之后,我们决定承担这个任务。数个月的艰辛工作之后,我们现在有了 MDB 的第一个稳定的 release。
MDB 给你提供了什么?
MDB 结合了 PEAR DB 和 Metabase 的大部分特性。实际上,PEAR DB 的特性中唯一不再存在的是作为结果集返回一个对象。我们放弃了这个特性是因为这个特性不常用而且对于性能的损失是非常明显的。许多开发上的时间用在了使得 API 尽可能的好用。最终,MDB 非常高地提供了这些功能至少和 PEAR DB 一样快而且比 Metabase 快很多。这些最重要地特性的列表:
OO 风格的 API
预准备的查询模拟
给所有传递进来以及从数据库中取出的数据的完全的数据类型抽象(包括 LOB 支持)
事务支持
数据库/表/索引/序列创建/抛弃/改变
RDBMS 无关的数据库 schema 管理
继承进 PEAR 框架(PEAR 安装程序,PEAR 错误处理等)
那么它如何使用呢?
MDB 提供了一些非常先进的抽象特性。记住这些特性只是供选择的是很重要的。但是在编写 RDBMS 无关的 PHP 程序时使用它们是非常重要的。一个展示使用 MDB 是多么简单的例子在文章的结尾的 "链接和文献" 部分。如前面所说,文章的焦点是介绍使得 MDB 与其他 PHP 数据库抽象层不同的那些特性。你可以在随本期文章一同包装的 CD 中找到所有这些例子脚本的代码。
但是,首先我们需要把 MDB 安装上去。使用 PEAR 安装程序这其实非常容易。我不能在这篇文章中完整的讲述 PEAR 安装程序但是我听说下一期将非常详细的讨论 PEAR 框架的里里外外。让安装程序运行于 Windows 的工作在进行当作但是支持仍然有一点古怪。对于 *nix 系统你需要 PHP 的 CGI 版本安装在了你的系统并且简单地运行下面地命令:
lynx -source go-pear.org|php
在安装完成之后你只需要再输入一行命令那么就全部搞定了。
pear install MDB
如果前面的过程对你来说不管用,总是有从 PEAR MDB 主页中直接获得包的选项。URL 列于文章的最后。
利用数据类型抽象
因为大部分数据库倾向于有一些个性或者怪癖,对于MDB来说把这些不同之处给开发者隐藏起来非常重要。MDB 通过定义自己的内部数据类型来达到这点:text,boolean,integer,decimal,float,date,time,time stamp,large objects(文件)。所有传递给数据库和从数据库获取的数据都能转换成 MDB 的内部格式或者从数据库的内部格式转化回来。本节相关的例子脚本能够再 datatype 目录中找到。让我们看看下面的查询:
$session = '098f6bcd4621d373cade4e832627b4f6';
// set time out to 30 minutes
$timeout = time()+60*30;
// SELECT query showing how the datatype conversion works
$query = 'SELECT createtime, user_id FROM sessions';
$query .= ' WHERE session = '.$session;
$query .= ' AND lastaccess
这个查询如果发送给数据库的话八成要失败。原因是存储在 $name 中的值需要转换为正确的字符串格式。这也许意味着 $name 的内容可能有特殊的转义字符或者被引号包围。PEAR DB 为此提供了方法 DB:.quote()。在 MDB 中这个方法叫 MDB::getTextValue()。不同之处是 MDB 给每种前面所列的数据类型都提供了这样的函数。因而我们也能够把 $timeout 转换为正确的格式。
// convert $timeout to the MDB timestamp format
$timeout = MDB_date::unix2Mdbstamp($timeout);
// SELECT query showing how the datatype conversion works
$query = 'SELECT createtime, user_id FROM sessions';
$query .= ' WHERE session = '.$mdb->getTextValue($session);
$query .= ' AND lastaccess getTimestampValue($timeout);
为了作个演示,让我们假定我仅仅想要获取第一行。MDB::queryRow() 获得第一行,它释放结果集并且返回其内容,因而它正是我们所要的。
$result = $mdb->queryRow($query);
但是不同的 RDBMS 返回像日期这样的数据时用的格式是不同的。因此,如果我们然后要对一些数据进行计算,不管选择的 RDBMS 是什么,把数据以相同的格式返回是重要的。这个可以由 MDB 半自动地完成。你所有需要做的是告诉你的结果列将是什么样的类型,MDB将处理转换的工作。最简单的办法是把这样的信息传递给查询函数。
$types = array('timestamp', 'integer');
$result = $mdb->queryRow($query, $types);
这告诉 MDB 结果集的第一列类型是 'timestamp' 以及第二列是'integer'。所有查询函数能够接受这样的元信息作为可选的参数。数据还能事后用 MDB::setResultTypes() 来设置。取决于数据获取于的数据库,它然后将被相应的转换返回的数据。MDB 内部的 timestamps 的数据格式是遵循 ISO 8601 标准的。其他像 PEAR::Date 这样的包能够处理这种格式。MDB 还在 MDB_Date 类中提供了一些数据格式转换函数,它们能够被可选的包含。
因为相当多的 RDBMS 以相同的方法返回整数数据,没有必要转换整数数据。因而,为了获得稍许的性能改进你能够这么做:
$types = array('timestamp');
$result = $mdb->queryRow($query, $types);
这样只有结果集的第一列会被转换。当然,如果 MDB 用于返回整数不同的数据库,这可能成为一个问题。然而,稍许的性能改善可能并不值得冒这个风险。但是再一次的,它显示了这些特性的使用仅仅是供选择的。
Listing 1 展示了一个使用预准备的查询的例子。如果你必须运行大量查询而唯一的差别是数据传递给数据库,但是查询的结构还是一样的,这些能够相当的方便。高级的数据库能够在内存中储存解析好的查询来加速性能。
Listing 1
$alldata = array(
array(1, 'one', 'un'),
array(2, 'two', 'deux'),
array(3, 'three', 'trois'),
array(4, 'four', 'quatre')
);
$p_query = $mdb->prepareQuery('INSERT INTO numbers VALUES (?,?,?)');
$param_types = array('integer', 'text', 'text');
foreach ($alldata as $row) {
$mdb->execute($p_query, NULL, $row, $param_types);
}
在 $alldata 中储存的所有四个数组将用于 execute 语句。数据将自动被转换为正确的格式。因为这是一个插入语句,MDB::execute() 的第二个参数被设置为 NULL 因为我们将没有任何结果列需要我们设置数据类型。
在支持的数据类型中还有 LOB (大对象),它使得我们能够在数据库中储存文件。二进制文件储存在 BLOB (二进制大对象)中而且普通文本文件储存在 CLOB (字符大对象)中。在 MDB 中你仅仅能够使用预准备的 INSERT 和 UPDATE 查询储存 LOB。使用 MDBA::setParamBlob() 或者 MDB::setParamClob() 你能够设置预准备查询的 LOB 域的值。两个函数都预期传递一个 LOB 对象,而它能够使用 MDB::createLob() 创建。
$binary_lob = array(
'Type' => 'inputfile',
'FileName' => './myfile.gif'
);
$blob = $mdb->createLob($binary_lob);
$character_lob = array(
'Type' => 'data',
'Data' => 'this would be a very long string container the CLOB data'
);
$clob = $mdb->createLob($character_lob);
如你能看到的,MDB::createLob() 被传递一个关系数组。Type 键的值可能是以下中的一个:data, inputfile 或者 outputfile。前两个用于你想要把 LOB 写入数据库的时候。如果你有一个储存在变量中的 LOB,你应当在 需要使用 inputfile 时从文件直接读取 LOB。最后,outpufile 应当在你想要从数据库中读取 LOB 时使用。取决于你是否使用数据或者 inputfile 你需要给 Filename 键或者 Data 键指定一个值,像上面的例子那样。现在,我们将把前面的 LOB 储存到数据库中去。
$p_query = $mdb->prepareQuery('INSERT INTO files (id, b_data, c_data) VALUES (1, ?, ?)');
$mdb->setParamBlob($p_query, 1 , $blob, 'b_data');
$mdb->setParamClob($p_query, 2 , $clob, 'c_data');
$result = $mdb->executeQuery($p_query);
为了从数据库中获取上面的文件,我们需要首先从数据库中选择数据并且使用 MDB::createLob() 创建 LOB 对象。这次我们将设置 'Type' 为 'outputfile'
$mdb->query('SELECT b_data FROM files WHERE id = 1');
$binary_lob = array(
'Type' => 'outputfile',
'Result' => $result,
'Row' => 0,
'Field' => 'b_data',
'Binary' => 1,
'FileName' => './myfile2.gif'
);
$blob = $mdb->createLob($binary_lob);
现在我们能够使用 MDB::readLob() 从结果集中读取 LOB。传递长度 0 给 MDB::readLob() 意味着整个 LOB 被读取和储存在我们前面指定的文件中。一旦任务完成了,我们可以把资源释放了。你也可以设置任何大于零的长度并且使用一个 while 循环检查 MDB::endofLob() 来读取 LOB。
$mdb->readLob($blob, $data, 0);
注意你不要把这个获取函数和 bulk 获取函数像 MDB::fetchAll()搞混了,因为这将在大部分 PHP 数据库扩展中导致问题。在一些时候,MDB 可能能够使用 bulk 获取函数获得 LOB。
如我们在这节所见,MDB 特性本身的原生数据类型集自动映射于数据库中的原生数据类型。这保证了无论我们发送和从数据库接收什么样的数据,它都能与使用的 RDBMS 无关的使用相同的格式。如我在本节开篇已经提到的,这明显需要数据库使用的数据类型是 MDB 预期的。这种需要被用于确保映射所耗费的代价很小。下一节将教给我们 MDB 如何辅助在数据库中使用正确的数据类型。
使用 XML schema 文件
利用在上个段落中描述的特性,你能编写真正的数据库独立的程序。但是 MDB 尝试向前更加迈出一步:它允许你用 XML 定义你的 schema。一个管理器把这种 schema 转换为给每种 RDBMS 的必要的 SQL 语句。这意味着你能对所有支持的 RDBMS 使用相同的 schema。本节的例子能够在 xml_schema 目录中找到。
我们现在将从头编写一个 XML schema 文件。首先,我们必须定义一个 XML 文档。数据库定义是包含在一个 database 标签之中的。数据库的名字是使用 name 标签定义的。create 标签告诉管理器数据库是否需要在它不存在的时候被创建。如果你把你的 schema 文件分割成好几个文件你你首先提交给管理器的那个文件中把 create 设置为 1。
可能你已经从数据库名 auth 猜出了这个数据库的目的是用于储存简单的验证程序的用户数据。Listing 2 定义了在其中我们能储存用户数据的表。
Listing 2