结构映射模式

当人们谈论对象-关系映射时,大部分的人都是在讨论结构映射模式,大部分模式都和Table Data Gateway无关,某些可以用在Row Data GatewayActive Record上,大部分都需要用在Data Mapper上。

映射关系

关键点是联系对象和关系的不同的方法,这会引出两个问题。第一个问题在表现(representation)上,对象保持引用而关系数据库保持的是键的关联。第二个问题是,对象可以很容易的使用集合来保持多个与它有关的其他对象的引用,但是关系数据库却正好相反,相关对象会有一个到主对象的反向的引用。比如一个部门有多个员工,部门对象持有多个员工的引用,但是再关系数据库中,每个员工的数据行中会有一个到部门表的外键连接,而不是在部门表中引用多个员工(因为关系数据库不支持这样做)。

解决表现问题的方法是在对象中保持一个Identity Field,使用它来作为关系数据库的键。当你访问数据库中的外键时,你使用的是Foreign Key Mapping来连接适当的对象间的连接。如果你没有在Identity Map中保存键,你需要回到数据库中去查找或者使用Lazy Load。每次保存对象的时候,你也要使用适当的键来保存它,对象间的连接也被目标对象的Identity Field替代。

在这个基础上处理集合需要更加复杂的Foreign Key Mapping。如果一个对象有一个其他对象的集合,你需要额外的查询来找出所有相关的数据(你也可以使用Lazy Load来将这个过程放到以后来做),每个相关的数据都成为对象并被加到集合中。保存这个集合需要一个个的保持,并且确保它们的外键都正确的指向目标对象。这将会很混乱,特别是你将不得不检测被加入和移除出集合的对象。这个处理方法显得很啰嗦,这也是为什么许多形式的基于元数据的方法成为大型系统中一个显著的改进。如果集合对象只在拥有集合的对象中使用,可以用Dependent Mapping来简化映射。

多对多的关系的情况与上面截然不同,关系数据库无法直接处理,所以你必须使用来创建一个新的关联表。

某些情况下关系的完整性使得更新变得更加复杂了。现代的系统允许你将关系完整性的检查推迟到事务的最后。另外数据库也会在每次写入的时候检查,这样你就需要特别注意更新顺序。一个可选的方法是硬编码更新表的顺序,这样也可以减少死锁的几率,避免事务频繁的回滚。

Identity Field通常用来讲内部对象引用转换成外键关联上,但是并不是所有的对象关系都必须以它来实现。小的Value Objects,如时间范围和金钱对象很明显不应该作为一个单独的表来存放。可以将所有的的子段取出并它们做为Embedded Value嵌入到链接对象中,读取和保存都将非常方便。

你也可以在一个大的范围中使用它,将对象序列化并当成Serialized LOB保存到表的一个列中。将一组对象序列化成XML文档时获得对象层次结构的一个好办法,这样你可以在一次查询中获取所有互相关联的小对象。通查数据库在查询小的互相有着高度关联的对象方面表现非常差--因为在这里你做了小而多的数据库查询。缺点是数据库不知道发生了什么,所以你不能在这个结构上做查询。又一次,XML发挥了作用,允许你在SQL查询中嵌入XPath查询表达式(虽然现在还没有标准)。通常Serialized LOB是最好的保存孤立对象在一起的方法。但是如果你过度的使用他,你的数据库就只比一个事务文件系统强那么一点点了。

继承

 除了上面提到的结构化对象,如树状机构,这种关系数据库比较处理的东西之外,还有一个OO中非常常见,但是关系数据库并不擅长处理的东西--继承。和上面一样,我们又要进行映射了,通常有三种映射方式:Single Table Inheritance,使用一张表来保存继承体系中所有的类;Concrete Table Inheritance,每个具体类使用一张表;Class Table Inheritance,每个类一张表。

选择那种方式通常需要在重复数据的数量和访问速度之间做平衡,Class Table Inheritance在类和表的关系上是最简单的,但是在读取数据的时候需要很多Join字句,这降低了效率。Concrete Table Inheritance避免了Join,可以从一张表中找出你要的对象,但是它对更改来说比较脆弱,对父类进行的修改必须反应到所有的子类中(当然映射代码也需要修改),修改继承结构本身更是会带来巨大的修改,而且,由于缺少抽象类,对于主键的管理会变得很奇怪并且没有数据库检查的保证(但是也有好处,减少了在抽象表中主键的锁冲突)。在许多数据库中,Single Table Inheritance最大的缺点是浪费了大量的空间,因为每一行都能容纳所有可能的子类,这样会有很多空闲的列,但是数据库在压缩空间方面做的很不错。另一个问题是他的大小,成为了访问的瓶颈,优点是把所有东西放在一起使得更新操作很方便。

这三种模式也并不是排外的,完全可以混合使用。如,常用的类可以用Single Table Inheritance,不常用的可以用Class Table Inheritance。当然,混合使用增加了复杂性。

这三者并不存在孰优孰劣的问题,根据你项目的需求来选择。作者的第一选择可能会是Single Table Inheritance,因为它比较简单而且更容易重构。更好的方法是讨教你的DBA,他们对于如何在数据库中存放数据更有发言权。

建立映射

映射到数据库时通常存在以下几种情况:

  • 你自己决定结构
  • 你必须映射到一个已存在的结构,并且不能修改
  • 你必须映射到一个已存在的结构,但是修改是可以商量的

最简单的情况是自己决定映射并且领域逻辑不是非常复杂,可能会是Transaction Script或者Table Module的设计。在这种情况下可以使用经典的数据库设计技术围绕数据来设计表,并使用Row Data Gateway或者Table Data Gateway来从领域逻辑中分隔SQL。

如果使用Domain Model,小心那些看上去像数据库设计的那些设计。因为如果你建立Domain Model时不考虑数据库的话,你可以最大程度上简化领域逻辑。把数据库设计当成持久化对象数据的方法。Data Mapper给予你最大的灵活性,但是也更复杂。如果数据库设计和Domain Model的设计是同构的,可以考虑使用Active Record来替代。

花六个月的时间建立一个独立于数据库的Domain Model,再持久化它是一个高风险的行为。危害在于设计可能会导致性能原因,修正它会花费相当长的时间。以更短的周期来建立数据库,这样你会得到更频繁且持续的反馈。

如果结构已经存在,你的选择就更少了,但是过程是多样的。对于简单的领域逻辑,你可以使用Row Data Gateway 或者Table Data Gateway类来模仿数据库,并在其上建立领域逻辑。如果领域逻辑更复杂一点,你需要Domain Model,它并不需要迎合数据库设计。只需逐步的建立Domain Model并用Data Mapper来把数据存储到已经存在的数据库中。

双重映射

有时我们会遇到需要从不同的数据源中获取数据的情况。可能是多个数据库,有着相同的数据,但是在结构上稍不同。另一种可能性是使用了不同的存储机制,如你可能需要从XML消息,CICS事务和关系表中读取数据。

最简单的方法是为不同的数据源使用多个映射层,然而,如果数据很相似的话会有很大的重复。这是你可能要考虑一种两步的映射结构。第一步将内存中的结构映射到一个逻辑的数据存储结构,第二步将逻辑的数据结构映射到真实的物理结构,第二步是根据不同的数据源而不同的。

 

使用元数据

简单而重复的映射会导致简单而重复的代码--闻到味道了,不是吗?用继承和委托把相同的行为提取出去才是好的OO实践,但是更好的方法是使用元数据映射--Metadata Mapping

Metadata Mapping将映射的细节放在了元数据文件中,如那个列对应那个对象的那个字段。这样就可以使用代码生成或者反射编程来避免了重复的编码。

当你使用Metadata Mapping,你需要有必要的基础设施,以便在内存中建立查询。Query Object允许你这样做而开发人员不需要知道SQL和关系数据库结构的细节。Query Object使用Metadata Mapping来转换相应的查询语句。

在这之后,你可以组成一个仓库(Repository),它把数据库很好的隐藏了起来.开发人员甚至不知道这个对象是不是从数据库中查询得到的。Repository和复杂的Domain Model系统一起工作的很好。

 

数据库连接

许多数据库接口依赖于某种形式的数据库连接对象来作为程序和数据库直接的连接。一个连接通常必须在你在数据库上执行命令之前被打开。事实上,你通常需要一个显式的连接来创建和执行命令,你执行命令的时候这个连接也是打开着的。查询返回一个Record Set,有些接口提供了断开连接的Record Sets,在连接被关闭之后仍然有效。另外一些接口只提供了连接着的Record Sets,就是说操作Record Sets时连接也必须被打开着。如果你在一个事务中运行,事务通常也是和特定的连接绑定的,当事务发生的时候连接叶必须被打开。

在许多环境中创建一个连接是昂贵的,所以建立一个连接池是有必要的。这时开发人员从连接池中请求一个连接并在用完之后释放回去,代替了打开和关闭连接的操作。当前的许多平台都提供了连接池,所以如果你真的必须自己创建一个,考虑一下是不是真的对性能有提高,在当前环境下,创建新连接变得越来越快,导致可能并不需要池了。

提供连接池的环境通常把它放在一个接口后面,看上去就像是创建一个新连接一样。这样你并不知道是得到了一个新创建的连接还是从池中得到的,这样做很好,是否使用池都被很好的封装了起来。接近的,关闭连接是真的关闭还是返回池中也被这样封装起来了。

不论创建连接是否昂贵,连接都必须被很好的管理起来。因为它们是昂贵的资源,必须在使用完之后尽快的被关闭。当使用事务的时候,确保所有的命令都是在同一个连接上执行的。

最常见的建议是显式的得到一个连接(通过池或者连接管理器),并用它来执行你想要执行的所有数据库命令,一旦用完,久关闭它。这个建议带来很多问题:所有需要连接的地方,确保你拥有它,并且确保你每次用完都要关闭它。

确保你在需要连接的地方都能得到它的方法有两种。一种是把连接作为显式的参数到处传递。令一种方法是在Registry中取出连接,既然你不想在多个线程中使用同一个连接,你可能会使用线程范围的Registry

如果你有我一半的健忘,显式关闭连接不是一个好办法。像连接一样,内存也是当用完之后需要关闭的资源,现代环境提供了自动内存管理和垃圾收集,所以确保连接被关闭的一个方法是使用垃圾收集器。这样的话连接本身以及引用它的其他对象都会在垃圾收集过程中关闭连接。坏处是连接只有在垃圾收集器启动的时候才会被关闭,也就是说没有被引用的连接在关闭之前会闲置一段时间。

我不喜欢依靠垃圾收集,其他的方案--甚至是显式关闭--都要更好。然而,在常规方案失效时,垃圾收集提供了一个很好的后备方案。不管怎么说,在挂起一段时间之后被关闭总比永远挂起要好。

既然连接和事务是如此紧密的联系在一起的,管理他们的好办法是开始事务的时候打开连接,结束事务的时候关闭连接。让事务知道它该使用哪个连接,这样你就可以完全忽略连接了。Unit of Work很适合用来管理事务和连接。

如果你在事务外做一些事情,比如读取不变的数据,使用一个新的连接,连接池可以很好的处理创建短生命周期的连接带来的问题。

posted on 2006-12-19 19:54  tmfc  阅读(2479)  评论(4编辑  收藏  举报