当前位置: 首页 > 图文教程 > 网络编程 > PHP > 《PHP设计模式介绍》第十六章 数据映射模式
前两章――动态数据模式与表数据网关模式各自展示对记录与每个表进行抽象的策略。这些模式都很有用,但每一个模式的执行都与底层的数据库结构结合过于紧密,因此基于以上模式的解决方案就存在一定的问题。比如,你的代码用字段名作为数组的关键字或是行数据对象的属性,则你的应用就受到数据库结构的约束,并且每当表结构发生哪怕是很小的变化,你都不得不在你的PHP程序中做大量的修改。
因为代码与数据库结构在开发过程经常变更,甚至在部署后也会发生。将代码与其数据库尽可能的分离,隔绝二者间的相互依赖性并减少因某一方的变化而产生的修改工作是非常有益的。
问题
你怎样才能将你的应用类与所属的数据库之间的结合度降至最低?例如,当你的数据表字段名发生变化时,你怎样将与此相关的修改工作降至最低?
解决方案
数据映射模式将对象的属性与存储它们的表字段间的结合密度降低。数据映射模式的本质就是一个类,它映射或是翻译类的属性或是方法到数据库的相应字段,反之亦然。数据映射的作用(工作)就在于能对双方所呈现出的信息的理解,并能对信息的存取进行控制,如根据存储在数据表中的信息重建新的域对象,或是用域对象的信息来更新或删除数据表中的相关数据。
对于面向对象代码与数据库表和字段间的映射关系的存储有多种实现方式。其中一种可能的方法就通过手工编码将这种映射关系存储在数据映射类中。另一种可选的方法是用PHP的数组并将其编码为类本身。这个类也能外源获取数据,如INI或是XML文件。
下图展示了一个数据映射类图,该类应用于解决存储URL书签(在前两章里已应用到)这类问题域。在图中,Bookmark对象是域对象,BookmarkMapper是数据映射模式的一个实现(执行)。Bookmark应该包含业务逻辑如校验URLs。BookmarkMapper则完全是一个在Bookmark的getter与setter方法与bookmark表字段结构间的交叉参照物。
这两个为关系很密切:BookmarkMapper充当了一个工厂,来实例化Bookmark,并且接受
Bookmark类的实例作为很多BookmarkMapper操作的参数。
样本代码
用UML示图作为路标,让我们来实现Bookmark与BookmarkMapper类。
首先,正如上面所提及,需要某种配置文件来处理表字段与对象方法间的映射。在本例中,我们用XML作为配置文件。
这个配置的主要目的是列示Bookmark表的字段,并指定哪个方法用于从Bookmark对象中存储与获取各自的信息。一个非常简单的XML结构就足够了,由一个<bookmark>根元素与一系列的<field>元素构成,如下所示
<name>元素存储实际的物理字段名。<accessor>元素存储了获取属性数据方法的名称,它是可选项,因为一些字段如时间戳是不需要映射的。<mutaror>则存储了Bookmark类中完成填充对象值的方法名。另一些信息也能添加到这个映射表中,例如,你能声明每个字段的thetype 和size元素,这使得你能用这些信息动态的生成SQL来建立数据表。如果你的应用有一个用PHP写的安装包,则你会对此特别感兴趣,这样你就可以通过这个映射表来建立表结构。当设定基于以上信息的PHP对象属性时,你也能自动的设定其数据值。)
完整的XML文件如下:
| <bookmark> |
| object(SimpleXMLElement)#21 (1) { |
这又导致实现Data Mapper另一个重要的要求:因为Data Mappe对域对象透明,所有相关的对象都必须对所有相关的属性提供某种公共的通道,这样Data Mapper在建立时才能正确初始化域对象,并在保存域对象时可读取其属性值。
Bookmark的属性都是保护型的,但给每个属性提供了getter和setter方法,因此这正好能满足需求。
让我们从设置与获取Bookmark类的’url’属性的代码开始。
| class Bookmark { protected $url; // ... public function getUrl() { return $this->url; } public function setUrl($url) { $this->url = $url; } } |
你能通过反射机制来避免的单调的编写无数简单的getter和setter方法。通过对对象自身的“窥探”,你能使对象测试某个特定的属性是否具有getters和setters以及是否这样命名的。
让我们来一些测试。
| 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()); } } } |
这段代码代码基于常例而不是某种直接的映射。获取与变更方法的名称由get与set开头,并由属性名组成(小写)。例如,获取’url’方法的名称为getUrl(),修改url方法的名称则为setUrl()。
这里是实现动态获取与修改方法的一些代码示例。
| class Bookmark { |
要达到动态生成getter 和setter方法,则方法名被分析,看是否是以’get’或是’set’开头,并且是否以一个正确的对象属性名结束。如果是这样,属性值就能正确的修改与返回。这个动态方法就可以代替手工实现的geturl()与geturl()方法了,这样,这些手工代码就可以删除了。
这儿有一个副作用要注意,如果用本代码调用了别的方法,则不会有错误信息给出。为了防止这种情况,让我们对错误的调用抛出一个例外出理。
| class Bookmark { |
| class BookmarkTestCase extends BaseTestCase { |
这儿还有另一个要注意的事项:一但当$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()); } } |
| class Bookmark { protected $id; //... public function setId($id) { if (!$this->id) { $this->id = $id; } } } |
以下代码测试了这种能力
| class BookmarkTestCase extends BaseTestCase { |
| class Bookmark { |
首先,我们为了实现BookmarkMapper,需要增加新的数据库记录。
在数据映射模式里面,域对象是对数据映射是透明的,但是它包含了所有的商业逻辑和创建对象潜在的规则。其中一个创建数据记录规则就是创建一个新的Bookmark对象实例,设置属性,还有让BookmarkMapper来保存新创建的对象实例。好,现在我们来看下如何实现这个接口。
BookmarkMapper 必须能够与数据库进行交互。就像在前面两个章节中所说的一样,我们使用
ADOdb 来访问数据库。此外,在构造BookmarkMapper的时候,把ADOdb的连接传递过去。
| //代码 class BookmarkMapper { protected $conn; public function __construct($conn) { $this->conn = $conn; } } |
BookmarkMapper 还必须导入刚才提到的XML 文件。为了让XML更方便使用,我们把映射存储为一些类的名字=> simplexml 元素。我们把这个加在构造函数里面:
| class BookmarkMapper { |
| class BookmarkMapperTestCase extends BaseTestCase { |
这里,测试代码创建了一个新的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()的的详细内容。你可以使用同样的BaseTestCase(前面的Table Data Gateway章节):
| 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()); } } |
从技术上来说,addSeveralBookmarks() 必须等待findById()工作正常再开始工作。(看save()方法里面的代码就知道了),我们等下再来研究addSeveralBookmarks()。
| class BookmarkMapper { |
| createBookmarkFromRow()。 |
| class BookmarkMapper { |
首先,数据从数据库提取出来;随后,建立一个Bookmark的实例。然后,对于映射的每一个部分,代码找到合适的setter方法并把数据行的值传递给setter方法。Bookmark实例,添加了数据库的数据后,用findById()取出。
现在,让我们看下BookmarkMapper::add()方法,通过BaseTestCase::addSeveralBookmarks()实现。通过一个测试用例,我们可以发现他们都是在表里面创建一个新的行并且返回Bookmark类的一个拥有实际数据的实例。
| class BookmarkMapperTestCase extends BaseTestCase { |
| 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; } } |
这个与动态的记录ActiveRecordTestCase::add()是很类似的,方便使用。但是这里它已经被加入到数据映射里面而不是测试用例,这样在整个项目里面都可以使用它。
你现在可以开始操作更多的finder方法,包括收集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类的实例。
怎么更新数据映射呢?更新的操作通用需要用到Bookmark和BookmarkMapper。确保bookmarks有没有更新最好的方法是使用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把新的bookmards插入到数据库。但是,就像这个测试用例涵盖的一样,save()现在必须确定Bookmark参数是新的或者已经增加到数据库里面了。对于前者,INSERT就可以操作了;对于后者,就需要用UPDATE了。
就目前的情况,让我们重构下操作INSERT语句的代码(这个原来是涵盖在save()方法里面的),成为一个新的私有的方法,命名为insert()。
| class BookmarkMapper { |
| class BookmarkMapper { //... public function save($bookmark) { if ($bookmark->getId()) { $this->update($bookmark); } else { $this->insert($bookmark); } } } |
现在,你还需要一个update() 方法,它和insert()方法很类似。回想一下,insert()方法按照固定的模式来编写代码从属性到域名进行数据映射。那么对于update(),让我们用一个更加动态的方法,从bookmark.xml里面获得信息并进行更改。
| class BookmarkMapper { |
最后,让我们看下“删除”的操作。我们为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())); } } |
现在,你可以通过数据映射模式来完整第进行操作了。
如果你的域对象创建起来比较繁琐,你可能需要写一个BookmarkMapper::deleteById()方法,它不需要加载域对象就能删除数据。
总结
很明显,在数据库和域对象之间增加一个转换层会造成一定的复杂性。但是,这个复杂性可以给你的代码带来巨大的灵活性,因为你可以不管数据库的表结构自由地升级你的类。
另外,你还需要记住的是所有这些例子还只是一个非常简单的转换机制。如果你需要对这个简单的机制进行升级,你可以参考holy grail of ORM—ObjectRelational Mapping—那里面会进行详细的阐述。
评论 (0) All