第十五章 表数据网关模式
前一章中使用动态记录模式对数据库表进行建立,获取,更新(通过扩展实现删除)每一行的操作。动态记录模式是一种简单的抽象数据库连接的方式,但是这种简洁性也正是它的弱点。动态记录类只处理单一的行,使得它在需要呈现大量信息的WEB应用中显得效率很低,如旅游预约,在线购物等。在这一类应用――几乎是主流的WEB应用中,数据集的使用就是更普遍更流行的。
问题
怎样才能简单的操作数据库表与表中的所有记录?
解决方案
表数据网关模式集成了动态记录模式。实际上,这个新模式的大多数代码都借签于14章动态记录模式的代码(它重用了一样的DB类与BOOKMARK TABEL DDL常量,并且也用ADOdb作为操纵数据的代码库)。然而,表数据网关模式集中于整表――记录集而不是单个的记录。
样本代码
让我们从建立操作开始,该操作完成向表中增加新记录。测试用例函数TableDataGatewayTestCase::testAdd() 完成向书签数据表中增加两条URL数据记录的步骤要求。它很大程度上参照了14章ActiveRecordTestCase::testAdd()方法,但它也其显著不同的地方,在于引入了一个新的BookmarkGateway这个表数据网关类。
class TableDataGatewayTestCase extends UnitTestCase {
function testAdd() {
$gateway = new BookmarkGateway($conn = DB::conn());
$gateway->add(
‘http://simpletest.org/’,
‘SimpleTest’,
‘The SimpleTest homepage’,
‘testing’);
$gateway->add(
‘http://blog.casey-sweat.us/’,
‘My Blog’,
‘Where I write about stuff’,
‘php’);
$rs = $this->conn->execute(‘select * from bookmark’);
$this->assertEqual(2,$rs->recordCount());
$this->assertEqual(2,$conn->Insert_ID());
}
}
类似于动态记录,表数据网关测试用例示例了一个模板类,并增加一些记录到数据库。然而表数据网关模的工作对象是整张表,你只需建立一个该模式对象,并重用该对象对就能向数据表中增加更多的新记录。
这儿是BookmarkGateway一个可行的实现。
class BookmarkGateway {
protected $conn;
public function __construct($conn) {
$this->conn = $conn;
}
const INSERT_SQL = “
insert into bookmark (url, name, description, tag, created, updated)
values (?, ?, ?, ?, now(), now())
“;
public function add($url, $name, $description, $group) {
$rs = $this->conn->execute(
self::INSERT_SQL
,array($url, $name, $description, $group));
if (!$rs) {
trigger_error(‘DB Error: ‘.$this->conn->errorMsg());
}
}
}
以上代码看上去很熟悉,动态记录模式与表数据网关模式的基本框架是相仿的:INSERT SQL 语句,函数参数表,对数据库错误的处理等都与动态记录模式的add()方法一次处理一条记录相类似。
建立了实现CRUD操作的代码后,现在来讨论如何获取数据。
测试用例结构
因为表数据网关的目的是处理具有多条记录的数据库表,你很有可能需要一个方便有效的方法来初始化表,使得在运行每一个实验时数据表都处于一个已知的状态。快速的解决方案是为每个实验建立一个基类,包括两个有用的方法:setup()与addSeveralBookmark,用来为每个实验重建已打乱的表和载入一些数据。
如下就是名为BaseTestCase的类
class BaseTestCase extends UnitTestCase {
protected $conn;
function __construct($name=’’) {
$this->UnitTestCase($name);
$this->conn = DB::conn();
}
function setup() {
$this->conn->execute(‘drop table bookmark’);
$this->conn->execute(BOOKMARK_TABLE_DDL);
}
function addSeveralBookmarks($gateway) {
// add(url, name, desc, tag)
$gateway->add(‘http://blog.casey-sweat.us/’
,’Jason\’s Blog’
,’PHP related thoughts’
,’php’);
$gateway->add(‘http://www.php.net/’
,’PHP homepage’
,’The main page for PHP’
,’php’);
$gateway->add(‘http://slashdot.org/’
,’/.’
,’News for Nerds’
,’new’);
$gateway->add(‘http://google.com/’
,’Google’
,’Google Search Engine’
,’web’);
$gateway->add(‘http://www.phparch.com/’
,’php|architect’
,’The home page of php|architect,
an outstanding monthly PHP publication’
,’php’);
}
}
现在,每一个测试用例都源自BaseTestCase并继承它的构造器,一个setup()方法与一个addSeveralBookmarks()方法来预装一些数据。
以数组形式返回记录集
无论什么时候实现一个表数据网关类,你都要选择一种数据结构来呈现从相关方法中获取的数据集。PHP中常用于表示此数据结构的术语是返回行的哈稀向量(以0为起始的数组),就是一个关联数组,由字段=>值成对组成。
在BookmarkGateway通过ADOconnection获得这样一个数据结构是很简单的,因为ADOResultSet::getArray()方法几乎采用的是相同的术语来表示(即返回一个关联数组)。
例如,这里findAll()可方法返回整个表数据网关类中表的内容。
class BookmarkGateway {
// ...
public function findAll() {
$rs = $this->conn->execute(‘select * from bookmark’);
if ($rs) {
return $rs->getArray();
} else {
trigger_error(‘DB Error: ‘.$this->conn->errorMsg());
}
}
}
简单与否,都需要测试一下:
class TableDataGatewayTestCase extends BaseTestCase {
// ...
function testFindAll() {
$gateway = new BookmarkGateway(DB::conn());
$this->addSeveralBookmarks($gateway);
$result = $gateway->findAll();
$this->assertIsA($result, ‘Array’);
$this->assertEqual(5, count($result));
}
}
如果你想做得更多,你可以检查一下返回的每一行。
class TableDataGatewayTestCase extends BaseTestCase {
// ...
function testFindAll() {
$gateway = new BookmarkGateway(DB::conn());
$this->addSeveralBookmarks($gateway);
$result = $gateway->findAll();
$this->assertIsA($result, ‘Array’);
$this->assertEqual(5, count($result));
$this->assertIsA($result[0], ‘Array’);
$this->assertEqual(7, count($result[1]));
$expected_keys = array(
‘id’
,’url’
,’name’
,’description’
,’tag’
,’created’
,’updated’);
$this->assertEqual(
$expected_keys
,array_keys($result[3]));
}
}
(索引0,1,和3 都是随机选的,可以是返回的五行中的任意一行),因为在返回的集合中的值是你初始化时存储的值(假设是这样),你也能建立一个实验来直接对数据值进行比较。
class TableDataGatewayTestCase extends BaseTestCase {
// ...
function testFindAll() {
$gateway = new BookmarkGateway(DB::conn());
$this->addSeveralBookmarks($gateway);
$result = $gateway->findAll();
// ...
$this->assertEqual(‘PHP homepage’, $result[1][‘name’]);
$this->assertEqual(‘http://google.com/’, $result[3][‘url’]);
}
}
返回可迭代的对象集合
数组是PHP内置的数据类型,并且PHP提供了大量的数组函数,使得在你的程序中使用数组非常方便。然而,你有可能希望以对象而不是数组的形式返回结果集。实际上,返回数据载体对象(数据的基本容器,也包含业务逻辑)的集合是很寻常的,ADOResultSET()方法恰好证实了这一点。让我们建立一个查找方法,可根据“tag”字段的值来查询数据,因为这个例子基于PHP5,让我们也要求返回的结果集可迭代(参见第八章,迭代模式),用PHP的foreach结构就能使用该结果集了。
(ADOdb默认返回的是行记录的散列数组,我有意让这个例子稍微复杂一点,来强制其返回一个数据转输对象,这样代码就有意思多了。并且你将看到这个示例应用了前面学习过的一些设计模式)。
以下测试用例是上述需求的简化表述。
class TableDataGatewayTestCase extends BaseTestCase {
// ...
function testFindByTag() {
$gateway = new BookmarkGateway(DB::conn());
$this->addSeveralBookmarks($gateway);
$result = $gateway->findByTag(‘php’);
$this->assertIsA($result, ‘AdoResultSetIteratorDecorator’);
$count=0;
foreach($result as $bookmark) {
++$count;
$this->assertIsA($bookmark, ‘ADOFetchObj’);
}
$this->assertEqual(3, $count);
}
}
findByTag()方法的实现如下
class BookmarkGateway{
// ...
public function findByTag($tag) {
$rs = $this->conn->execute(
‘select * from bookmark where tag like ?’
,array($tag.’%’));
return new AdoResultSetIteratorDecorator($rs);
}
}
很典型的,findByTag()首先调用execute()方法生成一个数据集。ADOdb的execute()方法带入两个参数,待执行的SQL语句和一个可选的梆定参数变量的数组。因为findByTag()需要用带通配符的LIKE操作,并且ADOdb会自动的给查询字串加引号,所以必须要给作为参数的数组在其内部就加上通配符。Execute()产生一个记录集后,AdoResultSetIteratorDecorator()将对其进行封包。AdoResultSetIteratorDecorator()的主要目的在于把结果集“转换”为可迭代的对象集合,也因此而得名。
ADOdb通过包含adodb-iterator.inc.php提供对迭代的支持。其中定义了一个ADODB_Iterator的类,其实质是将ADOResultSet修饰成为PHP5的一个迭代接口标准库。这使得你可以快速的形成一个可以遍历的结果集了。然而,迭代器的默认行为还是返回一个聚合数组。正如你将在下述试验中看到的那样。
class AdoResultSetIteratorDecoratorTestCase extends BaseTestCase {
function testADOdbDecorator() {
$gateway = new BookmarkGateway($this->conn);
$this->addSeveralBookmarks($gateway);
$rs = $this->conn->execute(‘select * from bookmark’);
foreach($rs as $row) {
$this->assertIsA($row, ‘array’);
$this->assertIsA($rs->fetchObj(), ‘ADOFetchObj’);
}
}
}
这儿,通过ADOdb迭代器,表数据就可以被建立,存储,迭代获取数据。
突出显示的代码行实际是无效,要注意避免。你的确能为每一行生成一个对象,如果这样,你就不得不在你的应用中到处重复这个笨拙的代码来实现对整个集合的迭代。
一个更好的解决方案――能更直接的满足对象集合迭代要求的是:修饰ADOdb迭代器。
测试外部库
写一个小测试用例来帮助你探测第三方库,更好的了解它们的特点。一系列的测试用例也能使你更好的把握住对外部库的依赖性(独立性),或是你的代码是如何特定的使用这些库,这样当库因升级而改变时能更快的找到并解决问题。
如果你担心对这些外部库的依赖性,则引入适配器(见第十三章--适配器模式)使你的代码从这种依赖关系中独立出来。
让我们写一个测试用例来演示迭代器是如何工作的。
class AdoResultSetIteratorDecoratorTestCase extends BaseTestCase {
// ...
function testRsDecorator() {
$gateway = new BookmarkGateway($this->conn);
$this->addSeveralBookmarks($gateway);
$rs = $this->conn->execute(‘select * from bookmark’);
$count=0;
foreach(new AdoResultSetIteratorDecorator($rs) as $bookmark) {
++$count;
$this->assertIsA($bookmark, ‘ADOFetchObj’);
$this->assertTrue($bookmark->id > 0);
$this->assertTrue(strlen($bookmark->url) > 10);
}
$this->assertEqual(5,$count);
}
}
以下代码说明了怎样改进(修饰)ADODB_Iterator来满足上述的需求。
require_once ‘adodb/adodb-iterator.inc.php’;
class AdoResultSetIteratorDecorator implements Iterator {
protected $rs;
public function __construct($rs) {
$this->rs = new ADODB_Iterator($rs);
}
public function current() {
return $this->rs->fetchObj();
}
public function next() {
return $this->rs->next();
}
public function key() {
return $this->rs->key();
}
public function valid() {
return $this->rs->valid();
}
public function rewind() {
return $this->rs->rewind();
}
}
上述代码中,大多数迭代器接口方法已作为代理来处理结果集了。但是current()方法被重载用于返回fetchObj()方法的结果。
回顾表数据网关,你应该理解findByTage()的工作原理了。
class BookmarkGateway {
// ...
public function findByTag($tag) {
$rs = $this->conn->execute(
‘select * from bookmark where tag like ?’
,array($tag.’%’));
return new AdoResultSetIteratorDecorator($rs);
}
}
更新记录
下面,让我们来解决CRUD中的“更新”。从概念上讲,你应该让表装满数据,找到一个数据对象,改变后保存它,并且再次找到该数据并校检更改是否存储。
返回到TableDataGatewayTestCase,这儿有查找记录的代码
class TableDataGatewayTestCase extends BaseTestCase {
// ...
function testUpdate() {
$gateway = new BookmarkGateway(DB::conn());
$this->addSeveralBookmarks($gateway);
$result = $gateway->findByTag(‘php’);
$bookmark = $result->current();
$this->assertIsA($bookmark, ‘ADOFetchObj’);
$this->assertEqual(
‘http://blog.casey-sweat.us/’
,$bookmark->url);
$this->assertEqual(
‘PHP related thoughts’
,$bookmark->description);
}
}
并且将代码改为如下所示:
class TableDataGatewayTestCase extends BaseTestCase {
// ...
function testUpdate() {
$gateway = new BookmarkGateway(DB::conn());
$this->addSeveralBookmarks($gateway);
$result = $gateway->findByTag(‘php’);
$bookmark = $result->current();
$this->assertIsA($bookmark, ‘ADOFetchObj’);
$this->assertEqual(
‘http://blog.casey-sweat.us/’
,$bookmark->url);
$this->assertEqual(
‘PHP related thoughts’
,$bookmark->description);
$new_desc = ‘A change to see it is updated!’;
$bookmark->description = $new_desc;
$gateway->update($bookmark);
}
}
改变后,重新查找该条记录并验证更新
class TableDataGatewayTestCase extends BaseTestCase {
// ...
function testUpdate() {
The Table Data Gateway Pattern 257
$gateway = new BookmarkGateway(DB::conn());
$this->addSeveralBookmarks($gateway);
$result = $gateway->findByTag(‘php’);
$bookmark = $result->current();
$this->assertIsA($bookmark, ‘ADOFetchObj’);
$this->assertEqual(
‘http://blog.casey-sweat.us/’
,$bookmark->url);
$this->assertEqual(
‘PHP related thoughts’
,$bookmark->description);
$new_desc = ‘A change to see it is updated!’;
$bookmark->description = $new_desc;
$gateway->update($bookmark);
$result = $gateway->findByTag(‘php’);
$bookmark = $result->current();
$this->assertEqual(
‘http://blog.casey-sweat.us/’
,$bookmark->url);
$this->assertEqual(
$new_desc
,$bookmark->description);
}
}
有了这样一个实验用例在手,现是在增加update()方法到BookmarkGateway类的时候了。
class BookmarkGateway{
// ...
const UPDATE_SQL = ‘update bookmark set url = ?
,name = ?
,description = ?
,tag = ?
,updated = now()
where id = ?’;
public function update($bookmark) {
$this->conn->execute(
self::UPDATE_SQL
,array(
$bookmark->url
,$bookmark->name
,$bookmark->description
,$bookmark->tag
,$bookmark->id
));
}
BookmarkGateway知道如何去执行SQL来更新数据,并能正确的将数据传输对象的属性的值映射到SQL语句相应的参数位置。
讨论
用表数据网关在对表进行操作,是与WEB应用中任务的执行更密切相关的。然而,表数据网关仍然与数据库表具体结构关系过于紧密(耦合)。将代码从表具体结构的依赖中独立出来将是下一章数据映射模式的主题。