The Data Mapper Pattern —— 数据映射模式

 Active Record 模式和 Table Data Gateway 模式 —— 分别介绍了抽象一个表行和一个单独的表的策略。尽管这两种模式都很有用,但它们的实现都与底层数据库结构紧密耦合,因此,基于这些模式的解决方法往往是很 脆弱的。例如,如果你的代码用字段名作数据行数组或对象的键和属性,那么你的应用程序就会与数据库结构绑在一起,对表中的每个(相关的)细微改动,你都不 得不在 PHP 代码中做大范围的更改。

因为代码和数据库在开发过程中经常变动,而且在部署后也会改进,所以尽可能地分离领域代码和它的数据库,隔离相互依赖性,减少实现一个改变所需的工作量是非常有好处的。

问题

如何将程序中的类和它的数据库之间的耦合降到最低?例如,如果改变表的一两个字段的名字,怎样才能把所需的修改降到最低?

解决方案

Data Mapper 模式解除了对象属性和保存它们的表字段之间的耦合。Data Mapper 模式的本质就是一个类,它把领域对象的属性和/或方法映射或转换为数据库表字段,反之亦然。Data Mapper 的工作就是,了解信息的表示方法并能够来回发送信息,基于数据库中的信息创建新的领域对象,以及利用领域对象的信息更新或删除数据库中的信息。

面向对象代码和数据库表及字段之间的映射可以以各种各样的形式存储。一种可能是把映射关系手工编码在 Data Mapper 类中。另一种选择是在类本身里面编写一个 PHP 数组。另外,也可以从一个外部资源提取信息,如 INI 文件或 XML 文件。

下图显示了一个 Data Mapper 模式的类图,这些类用于解决前两章所用到的存取 URL 书签的问题域。图中,Bookmark 是领域对象,BookmarkMapper 是 Data Mapper 模式的一个实现。Bookmark 应该包含象验证 URL 那样的业务逻辑。BookmarkMapper 则扮演 Bookmark 的 getter 和 setter 方法与书签表字段结构之间的一个完整的交叉参照

这两个类紧密相关:BookmarkMapper 充当了 Bookmark 实例的一个 Factory,并且接受 Bookmark 类的实例作为大量 BookmarkMapper 操作的参数。

范例代码

以 UML 图为引导,我们来实现这两个类,BookmarkBookmarkMapper

首先,如上所述,需要某种配置来处理表字段和对象方法之间的映射。本例中,我们使用一个 XML 配置文件。

这个配置的目的是列出 bookmark 表的字段,并指定哪个方法用于设置和从 Bookmark 对象提取各自的信息。一个非常简单的包,含一个 <bookmark> 根元素和一系列 <field> 元素的 XML 格式就足够了,如下所示:

<field>
<name>url</name>
<accessor>getUrl</accessor>
<mutator>setUrl</mutator>
</field>

<name> 元素保存了实际的物理数据库字段名。<accessor> 元素用来命名获取属性的方法,而且是可选的,因为有些字段,如时间戳,并不需要映射。<mutator> 元素保存了装配对象值时要用到的 Bookmark 方法。

(也可以在这个映射中增加其它信息。例如,你也可以声明每个字段的类型和大小,并用这些信息动态生成建立数据表所需的 SQL。如果你的程序有一个用 PHP 写的安装程序包,那么你可能会对此特别感兴趣,这样你就可以用映射来建立数据表结构了。当基于这样的信息设置 PHP 对象的属性时,你还可以自动地类型化数字和日期字段。)

完整的 XML 文件如下:

<bookmark>
<field>
<name>id</name>
<accessor>getId</accessor>
<mutator>setId</mutator>
</field>
<field>
<name>url</name>
<accessor>getUrl</accessor>
<mutator>setUrl</mutator>
</field>
<field>
<name>name</name>
<accessor>getName</accessor>
<mutator>setName</mutator>
</field>
<field>
<name>description</name>
<accessor>getDesc</accessor>
<mutator>setDesc</mutator>
</field>
<field>
<name>tag</name>
<accessor>getGroup</accessor>
<mutator>setGroup</mutator>
</field>
<field>
<name>created</name>
<mutator>setCrtTime</mutator>
</field>
<field>
<name>updated</name>
<mutator>setModTime</mutator>
</field>
</bookmark>

我们可以利用 PHP5 中名为 SimpleXML 的特性来读取和解析这个文件。你所需要做的就是调用 simplexml_load_file('bookmark.xml'),于是你就有了一个现成的,包含 XML 文件的所有信息的 SimpleXMLElement 复合对象。对于这个例子,其结果如下:

object(SimpleXMLElement)#21 (1) {
["field"]=>
array(7) {
[0]=>
object(SimpleXMLElement)#15 (3) {
["name"]=>
string(2) "id"
["accessor"]=>
string(5) "getId"
["mutator"]=>
string(5) "setId"
}
[1]=>
object(SimpleXMLElement)#19 (3) {
["name"]=>
string(3) "url"
["accessor"]=>
string(6) "getUrl"
["mutator"]=>
string(6) "setUrl"
}
//...<snip>...
[4]=>
object(SimpleXMLElement)#23 (3) {
["name"]=>
string(3) "tag"
["accessor"]=>
string(8) "getGroup"
["mutator"]=>
string(8) "setGroup"
}
//...<snip>...
}

由于这个 XML 文件在领域空间和数据库空间之间建立了映射,所以 BookmarkMapper 在创建时会读取这个 XML 配置文件。

在正式研究 BookmarkMapper 之前,我们先深入研究一下 Bookmark 类。假设 Bookmark 在现有项目中已经被大量使用了,那么最好尽可能少的影响它。而且,Bookmark 不应该仅仅为了适应 BookmarkWapper 而修改。实际上,Data Mapper 模式就是旨在亲和。领域对象本身对 Data Mapper 的存在完全透明。

这又导致了实现 Data Mapper 模式的另一个重要的需求:因为每个领域对象对 Data Mapper 完全透明,所有相关的领域对象必须为相关属性提供某种公共访问,以便 DataMapper 在创建领域对象的时候能够正确地初始化对象,并在保存领域对象的时候能够读取属性。Bookmark 的所有属性都定义为 protected,但为每个属性都提供了 getter 和 setter 方法,所以它满足需要。

我们从编写设置和获取 Bookmark 类的“url”属性的代码开始。

class Bookmark {
protected $url;
// ...

public function getUrl() {
return $this->url;
}

public function setUrl($url) {
$this->url = $url;
}
}

可以利用 reflection(映射) 避免单调地编写无数简单的 getter 和 setter 方法。通过对对象自身的“窥探”,可以让对象来决定是否一个特定的属性应该有或没有 getter 和 setter 方法,以及这些方法应该如何命名。

我们从一些测试开始:

class BookmarkTestCase extends BaseTestCase {
//...

function testAccessorsAndMutators() {
$bookmark = new Bookmark(false);

$props = array('Url', 'Name', 'Desc', 'Group', 'CrtTime', 'ModTime');
foreach ($props as $prop) {
$getprop = "get$prop";
$setprop = "set$prop";

$this->assertNull($bookmark->$getprop());

$val1 = 'some_val';
$bookmark->$setprop($val1);
$this->assertEqual($val1, $bookmark->$getprop());

$val2 = 'other_val';
$bookmark->$setprop($val2);
$this->assertNotEqual($val1, $bookmark->$getprop());
$this->assertEqual($val2, $bookmark->$getprop());
}
}
}

对每个 Bookmark 属性,这个测试都通过 mutator 方法设定了一个值,然后验证 accessor 方法返回相同的值。然后值再次被改变并验证。

这段代码依赖惯例而非某种明确的映射。获取和变更方法的名称分别以 getset 打头,然后跟属性名(小写形式)组成。例如,“url”的获取方法的名称是 getUrl();修改方法是 setUrl()

下面是实现动态获取和修改方法的一些代码。

class Bookmark {
protected $url;
protected $name;
protected $desc;
protected $group;
protected $crttime;
protected $modtime;
//...

public function __call($name, $args) {
if (preg_match('/^(get|set)(\w+)/', strtolower($name), $match)
&& $attribute = $this->validateAttribute($match[2])) {
if ('get' == $match[1]) {
return $this->$attribute;
} else {
$this->$attribute = $args[0];
}
}
}

protected function validateAttribute($name) {
if (in_array(strtolower($name),
array_keys(get_class_vars(get_class($this))))) {
return strtolower($name);
}
}
}

这段代码依赖 PHP5 的“魔术”方法 __call(),不管什么时候,只要一个未定义(没有在类中明确定义)实例方法被调用,它就会调用。__call() 本质上是一个回调方法。被调用的方法的名字(未定义的)作为第一个参数被传递给 __call(),而任何方法参数以一个数组的形式作为第二个参数被传入。

为了实现动态创建 getter 和 setter 方法,被调用的方法的名字被提取出来,看它是否以“get”或“set”开始,并且以一个正确的对象属性名结束。如果是这样的话,属性就会被正确地修改或返回。这种动态方式代替了手工编写 getUrl()setUrl(),因此它们可以安全地从代码中删除了。

可是,有一个副作用需要注意:如果对这段代码调用别的方法,不会给出错误信息。为了防止这种情况,如果调用方法无效,就抛出一个异常。

class Bookmark {
//...

public function __call($name, $args) {
if (preg_match('/^(get|set)(\w+)/', strtolower($name), $match)
&& $attribute = $this->validateAttribute($match[2])) {
if ('get' == $match[1]) {
return $this->$attribute;
} else {
$this->$attribute = $args[0];
}
} else {
throw new Exception('Call to undefined method Bookmark::'.$name.'()');

}
}
}

你也可以对这个异常进行测试:

class BookmarkTestCase extends BaseTestCase {
//...

function testBadGetSetExceptions() {
$mapper = new BookmarkMapper($this->conn);
$this->addSeveralBookmarks($mapper);

$bookmark = $mapper->findById(1);

try {
$this->assertNull($bookmark->getFoo());
$this->fail('no exception thrown');
} catch (Exception $e) {
$this->assertWantedPattern('/undefined.*getfoo/i',
$e->getMessage());
}

try {
$this->assertNull($bookmark->setFoo('bar'));
$this->fail('no exception thrown');
} catch (Exception $e) {
$this->assertWantedPattern('/undefined.*setfoo/i',
$e->getMessage());
}
}
}

这里还需要注意一点:$id 属性一旦设置了就应该不能再改了。

让我们为不变的 ID 属性创建一个测试。setId() 能被调用一次来设置 ID,然后就可以反复使用 getId() 来获取其值,但随后对 setId() 的调用就应该无效了。

class BookmarkTestCase extends BaseTestCase {
//...

function testUnsetIdIsNull() {
$bookmark = new Bookmark;
$this->assertNull($bookmark->getId());
}

function testIdOnlySetOnce() {
$bookmark = new Bookmark;

$id = 10; //just a random value we picked
$bookmark->setId($id);
$this->assertEqual($id, $bookmark->getId());

$another_id = 20; // another random value, != $id
//state the obvious
$this->assertNotEqual($id, $another_id);
$bookmark->setId($another_id);

// still the old id
$this->assertEqual($id, $bookmark->getId());
}
}

应该记住一个非常重要的规则,在类中显示定义的方法会重载由 __call() 执行的同名方法。只需要给类增加一个指定的方法,你就可以为任何指定的方法定义一个特殊的,完全不同的行为。如下所示,setId() 将会重载任何对 __call() 的反复调用。

class Bookmark {
protected $id;
//...

public function setId($id) {
if (!$this->id) {
$this->id = $id;
}
}
}

迄今为止,我们得到的只是一个基本的数据对象,因此,让我们加入一些领域逻辑 —— 毕竟,应用 Data Mapper 模式的原因之一就是把领域逻辑从领域对象的持久性存储中分离出来。为了与“告知,而不是询问”设计原则保持一致,增加一个 fetch() 方法来返回书签页面的实际(HTML)内容。

下面是这个功能的一个测试:

class BookmarkTestCase extends BaseTestCase {
//...

function testFetch() {
$bookmark = new Bookmark;
$bookmark->setUrl('http://www.google.com/');

$page = $bookmark->fetch();

$this->assertWantedPattern(
'~<input[^>]*name=q[^>]*>~im',
$page
);
}
}

下面是一个实现示例:

class Bookmark {
//...

public function fetch() {
return file_get_contents($this->url);
}
}

现在,完整的类如下所示:

class Bookmark {
protected $id;
protected $url;
protected $name;
protected $desc;
protected $group;
protected $crttime;
protected $modtime;


public function setId($id) {
if (!$this->id) {
$this->id = $id;
}
}

public function __call($name, $args) {
if (preg_match('/^(get|set)(\w+)/', strtolower($name), $match)
&& $attribute = $this->validateAttribute($match[2])) {
if ('get' == $match[1]) {
return $this->$attribute;
} else {
$this->$attribute = $args[0];
}
} else {
throw new Exception('Call to undefined method Bookmark::'.$name.'()');
}
}

protected function validateAttribute($name) {
if (in_array(strtolower($name),
array_keys(get_class_vars(get_class($this))))) {
return strtolower($name);
}
}

public function fetch() {
return file_get_contents($this->url);
}
}

掌握了 Bookmark 类,我们再回到 BookmarkMapper 类。BookmarkMapper 的核心任务是从数据库取出数据,并创建 Bookmark 对象。

BookmarkMapper 要完成的第一个任务是给数据表添加新记录。

在 Data Mapper 模式中,领域对象对 Data Mapper 完全透明,但它包含了所有的业务逻辑,以及有关对象创建的潜在规则。一个合理的用于创建记录的方法然后又用来创建一个 Bookmark 类的新的实例,设置属性,然后请求 BookmarkMapper 保存这个新建的实例。我们就来实现一个这样的接口。

BookmarkMapper 必须与数据库进行交互。就像前两章那样,我们还是用 ADOdb 作数据库访问层。此外,我们还在创建 BookmarkMapper 的时候传递一个 ADOdb 连接。

class BookmarkMapper {
protected $conn;


public function __construct($conn) {
$this->conn = $conn;
}
}

BookmarkMapper 还必须读取前面那个 XML 文件。为了使 XML 更加易于使用,把映射保存为一个“name => 映射文件中每个子段的 simplexml 对象”哈希表。把这个功能增加到构造函数于是就得到了:

class BookmarkMapper {
protected $map = array();
protected $conn;


public function __construct($conn) {
$this->conn = $conn;
foreach (simplexml_load_file('bookmark.xml') as $field) {
$this->map[(string)$field->name] = $field;
}
}
}

现在,该给 save() 方法创建一个测试用例了。

class BookmarkMapperTestCase extends BaseTestCase {
function testSave() {
$bookmark = new Bookmark;
$bookmark->setUrl('http://phparch.com/');
$bookmark->setName('php|architect');
$bookmark->setDesc('php|arch magazine homepage');
$bookmark->setGroup('php');

$this->assertNull($bookmark->getId());

$mapper = new BookmarkMapper($this->conn);
$mapper->save($bookmark);

$this->assertEqual(1, $bookmark->getId());
// a row was added to the database table
$this->assertEqual(
1,
$this->conn->getOne('select count(1) from bookmark')
);
}
}

这里,测试创建了一个新的 Bookmark 类的实例,并设置了相关的对象属性,然后请求 BookmarkMapper 实例来 save() Bookmark。跟着,这个测试还验证了保存对象的同时也设置了它的 ID,并插入了一条记录到数据库。

接下来,我们编写一些代码来实现它。

class BookmarkMapper {
//...

const INSERT_SQL = "insert into bookmark (url, name,
description, tag, created, updated)
values (?, ?, ?, ?, now(), now())";

public function save($bookmark) {
$rs = $this->conn->execute(self::INSERT_SQL,
array($bookmark->getUrl(),

$bookmark->getName(),
$bookmark->getDesc(),
$bookmark->getGroup()));
}
}

一个类常量保存了用来执行插入操作的语句,代码“手动”把 Bookmark 类的 accessor 方法映射到 SQL 语句中对应的绑定值。

一切都还不错,但还需要做两件事:分别是编写用来处理数据库错误的代码,以及设置或修改被数据库初始化或改变的 $bookmark 属性的代码。

class BookmarkMapper {
//...

public function save($bookmark) {
$rs = $this->conn->execute(
self::INSERT_SQL,
array($bookmark->getUrl(),
$bookmark->getName(),
$bookmark->getDesc(),
$bookmark->getGroup()));

if ($rs) {
$inserted = $this->findById($this->conn->Insert_ID());

//clean up database related fields in parameter instance
$bookmark->setId($inserted->getId());
$bookmark->setCrtTime($inserted->getCrtTime());
$bookmark->setModTime($inserted->getModTime());
} else {
throw new Exception('DB Error: '.$this->conn->errorMsg());
}

}
}

findById() 很快就会显示,但它的目的是查找并返回与给定 ID 匹配的 Bookmark。本质上,BookmarkMapper 用于插入新的 Bookmark,从数据库中取出记录,然后基于新的正确值来设置合适的属性。因为 Bookmark 实例本身就是参数,而且已经被恰当地修改了,所以不需要返回任何东西。

我们继续来看 findById() 方法的细节。你可以使用与前面 Table Data Gateway 一章相同的 BaseTestCase

class BookmarkMapperTestCase extends BaseTestCase {
// ...

function testFindById() {
$mapper = new BookmarkMapper($this->conn);
$this->addSeveralBookmarks($mapper);

$this->assertIsA($bookmark = $mapper->findById(1),
'Bookmark');
$this->assertEqual(1, $bookmark->getId());
}
}

从技术上来说,除非 findById() 运行,否则 addSeveralBookmarks() 也不会运行(因为代码只出现在 save() 方法中),但我们还是花几分钟来了解一下。

class BookmarkMapper {
// ...

public function findById($id) {
$row = $this->conn->getRow('select * from bookmark where id = ?',
array((int)$id));
if ($row) {
$bookmark = new Bookmark($this);

foreach ($this->map as $field) {
$setprop = (string)$field->mutator;
$value = $row[(string)$field->name];
if ($setprop && $value) {
call_user_func(array($bookmark, $setprop), $value);
}
}

return $bookmark;

} else {
return false;
}
}
}

既然映射程序中的每个查找方法都必须把一个数据行转化为一个 Bookmark 实例,最好把这个功能提取到一个独立的方法中,命名为 createBookmarkFromRow()

class BookmarkMapper {
// ...

protected function createBookmarkFromRow($row) {
$bookmark = new Bookmark($this);

foreach ($this->map as $field) {
$setprop = (string)$field->mutator;
$value = $row[(string)$field->name];
if ($setprop && $value) {
call_user_func(array($bookmark, $setprop), $value);
}
}

return $bookmark;
}

}

通过这个方法,可以把 findById() 简化为:

class BookmarkMapper {
// ...

public function findById($id) {
$row = $this->conn->getRow('select * from bookmark where id = ?',
array((int)$id));
if ($row) {
return $this->createBookmarkFromRow($row);
} else {
return false;
}
}
}

所有这些都有些复杂,因此,一个 UML 顺序图可能对理解有所帮助。

首先,从数据库中取出数据;接着,创建一个 Bookmark 类的实例。然后,对于映射中的每个字段,代码找到合适的 setter 方法,并把记录行的值传递给 setter。Bookmark 实例,现在已经包含了数据库数据,于是,可以通过 findById() 返回了。

现在,我们来看 BookmarkMapper::add() 方法,它被 BaseTestCase::addSeveralBookmarks() 所使用。通过一个测试用例,可以验证它在表中创建了一条记录,并且返回一个包含了正确的映射数据的 Bookmark 类的实例。

class BookmarkMapperTestCase extends BaseTestCase {
// ...

function testAdd() {
$mapper = new BookmarkMapper($this->conn);
$bookmark = $mapper->add('http://phparch.com',
'php|arch',
'php|architect magazine homepage',
'php');
$this->assertEqual(1, $this->conn->getOne('select count(1) from bookmark'));
$this->assertEqual('http://phparch.com', $bookmark->getUrl());
$this->assertEqual('php|arch', $bookmark->getName());
$this->assertEqual('php|architect magazine homepage',

$bookmark->getDesc());
$this->assertEqual('php', $bookmark->getGroup());
}
}

下面就是 BookmarkMapper 的相关代码。

class BookmarkMapper {
// ...

public function add($url, $name, $description, $group) {
$bookmark = new Bookmark;
$bookmark->setUrl($url);
$bookmark->setName($name);
$bookmark->setDesc($description);
$bookmark->setGroup($group);

$this->save($bookmark);

return $bookmark;
}
}

这类似于 Active Record 的 ActiveRecordTestCase::add() 方法,但是在这里,它已经被增加到映射程序,而不是测试用例,使得它可以在项目代码中使用。

现在,你可以继续实现其它查找方法,包括返回 Bookmark 实例集合的方法。

class BookmarkMapperTestCase extends BaseTestCase {
// ...

function testFindByGroup() {
$mapper = new BookmarkMapper($this->conn);
$this->addSeveralBookmarks($mapper);

$this->assertIsA($php_links = $mapper->findByGroup('php'),
'array');
$this->assertEqual(3, count($php_links));

foreach ($php_links as $link) {
$this->assertIsA($link, 'Bookmark');
}
}
}

查找一个具体分类中的所有 bookmarks 可以实现为:

class BookmarkMapper {
// ...

public function findByGroup($group) {
$rs = $this->conn->execute('select * from bookmark where tag like ?',
array($group.'%'));
if ($rs) {
$ret = array();
foreach ($rs->getArray() as $row) {
$ret[] = $this->createBookmarkFromRow($row);
}


return $ret;
}
}
}

ADOConnection::execute() 方法返回一个 ADOResultSet 对象。这个结果集包含一个 getArray() 方法,用于为每个数据行返回一个联合数组(field => value)。然后,这些数据行数组依次传递给 createBookmarkFromRow() 方法来创建 Bookmark 类的实例。

如何在映射程序中进行更新呢?更新操作同样是 BookmarkBookmarkMapper 之间的协作。确保 bookmark 确实有没有更新最好的方法是用 BookmarkTestCase。而数据库的往返测试则由 BookmarkMapper 的测试负责。

class BookmarkTestCase extends BaseTestCase {
// ...

function testSaveUpdatesDatabase() {
$mapper = new BookmarkMapper($this->conn);
$this->addSeveralBookmarks($mapper);

$bookmark = $mapper->findById(1);
$this->assertEqual('http://blog.casey-sweat.us/',
$bookmark->getUrl());

$bookmark->setUrl('http://blog.casey-sweat.us/wp-rss2.php');
$mapper->save($bookmark);
$bookmark2 = $mapper->findById(1);

$this->assertEqual('http://blog.casey-sweat.us/wp-rss2.php',
$bookmark2->getUrl());
}
}

当前,save() 方法通过 INSERT 把新的 bookmark 插入到数据库中。然而,正如这个测试实例所暗示的,save() 现在必须确定 Bookmark 参数是新的还是之前就已经添加到数据库的。对于前者,INSERT 就可以了;而对于后者,就需要使用 UPDATE 了。

就目前的情况,我们重构执行 INSERT 语句的代码,它们位于 save() 方法中,把它们移到一个新的叫做 insert()protected 方法中。

class BookmarkMapper {
//...

protected function insert($bookmark) {
$rs = $this->conn->execute(self::INSERT_SQL,
array($bookmark->getUrl(),
$bookmark->getName(),
$bookmark->getDesc(),
$bookmark->getGroup()));
if ($rs) {
$inserted = $this->findById($this->conn->Insert_ID());
// clean up database related fields in parameter instance
if (method_exists($inserted, 'setId')) {
$bookmark->setId($inserted->getId());
$bookmark->setCrtTime($inserted->getCrtTime());
$bookmark->setModTime($inserted->getModTime());
}
} else {
throw new Exception('DB Error: '.$this->conn->errorMsg());
}
}
}

把现有的 save() 方法重命名为 insert() 的同时,save() 方法(还需要编写)必须使用 getId() 检查 $id 属性是否已经被设置了:

class BookmarkMapper {
//...

public function save($bookmark) {
if ($bookmark->getId()) {
$this->update($bookmark);
} else {
$this->insert($bookmark);
}
}
}

现在,你需要一个与 insert() 方法类似的 update() 方法。如果你回想一下,insert() 方法硬编码了属性到字段名的映射。对于 update(),我们使用一个更加动态的方法,使用来自 bookmark.xml 映射文件的信息。

class BookmarkMapper {
//...

const UPDATE_SQL = "update bookmark set url = ?,
name = ?,
description = ?,
tag = ?,
updated = now() where id = ?";

protected function update($bookmark) {
$binds = array();
foreach (array('url', 'name', 'description', 'tag', 'id') as $fieldname) {
$field = $this->map[$fieldname];
$getprop = (string)$field->accessor;
$binds[] = $bookmark->$getprop();
}

$this->conn->execute(self::UPDATE_SQL, $binds);
}
}

注意,数组中元素的顺序与我们的 SQL 语句中所需的顺序是一致的。update() 方法反映了 Data Mapper 的本质。它在属性和字段之间建立了一种联系。

最后,我们来看“删除”(delete) CRUD 功能的实现。我们为 BookmarkMapper 类编写一个方法,它接受一个 Bookmark 并把它从数据库中删除。

首先是一个测试:

class BookmarkMapperTestCase extends BaseTestCase {
// ...

function testDelete() {
$mapper = new BookmarkMapper($this->conn);
$this->addSeveralBookmarks($mapper);

$this->assertEqual(5, $this->countBookmarks());

$delete_me = $mapper->findById(3);
$mapper->delete($delete_me);

$this->assertEqual(4, $this->countBookmarks());
}

function countBookmarks() {
return $this->conn->getOne('select count(1) from bookmark');
}
}

然后是代码:

class BookmarkMapper {
// ...

public function delete($bookmark) {
$this->conn->execute('delete from bookmark where id = ?',
array((int)$bookmark->getId()));
}

}

现在,你已经为 bookmark 表实现了一个包含了完整的 CRUD 功能的 Data Mapper 模式。

如果领域对象的创建开销很大,那么你可能需要编写一个 BookmarkMapper::deleteById() 方法,它不需要加载领域对象就可以删除记录。

小结

毫无疑问,在数据库结构和领域对象之间添加一个交换层增加了一定的复杂性。然而,这个复杂性可以为你的代码带来巨大的灵活性,因为你可以不管数据表结构而自由地升级你的类。

另外,你也应该记住,所有这些还只是非常简单的交换机制,如果你想改进这种机制,用于处理领域模型中的表和与之相应的关系,那么你正在向 ORM —— Object Relational Mapping 迈进 —— 它可不是轻易可以涉足的哟。

AddThis Social Bookmark Button

相关文档(Relevant Entries)
广告管理系统的UML分析与设计
单例模式
抽象工厂模式
工厂方法模式
Inversion of Control Containers and the Dependency Injection pattern
The Model-View-Controller Pattern —— MVC 模式
设计模式与实现
常用的几中设计模式
WoW Powerleveling
Posted on November 27, 2006 2:46 PM | | | Comments (0) | | TrackBacks (0)

引用地址(TRACKBACKS)
 
TrackBack URL for this entry:
http://www.wujianrong.com/mt-tb.cgi/4275

发布评论(ADD YOUR COMMENTS)
 
感谢您参与评论;发表您的意见时请保持文章的相关性;不相关的或是单纯宣传的内容可能会被删掉。您的E-mail只是用来确认您发表的文章,不会出现在网页上。
Please keep your comments relevant to this blog entry. Email addresses are never displayed, but they are required to confirm your comments.

称呼(Name):      记住我的个人信息(Remember)
邮箱(Email):
网址(URL):
评论(Add your comments):

相关内容
广告计划