简述
在单个组件中封装域对象和关系数据之间的映射。对象/关系映射同时把应用程序代码和域对象从底层的数据模型和数据访问细节中分离出来。
背景
程序员使用面向对象概念以自然的、熟悉的方式对现实世界中的实体建模。对象的行为使它能够使用应用程序领域的语义与其他对象交互。通过聚合与继承相连的域对象共同实现了优雅的应用程序级解决方案。
当需要读写域对象所表示的关系数据时,应用程序的简洁性常常受到兵贵损害。这是因为直接使用关系数据对现实世界建模通常是不可能的。相反,现实世界的实体必须变形为行和列的格式。这种格式与面向这对象模型所提供的丰富性形成了鲜明的对比。
比如,考虑一个订单处理应用程序中所涉及的一些对象。订单包括惟一的订单号、下订单的客户号、客房下订单的日期以及组成订单的0个或多个条目。类似的,一个条目由项目号、产品号、客户订购的数量和产品单价组成。图1表示定义该用程序域对象的Order和LineItem类。
数据模型包括ORDERS和LINE_ITEMS表,它们定义了类似的概念,不过这些表之间的关系不是那么明显,如图2所示。因为一个订单包含0个或多个条目,所以在ORDERS表和LINE_ITEMS表之间存在一对多的关系。为了建立表之间的联系,LINE_ITEMS把ORDER_ID列定义为外键,可以使用它联接ORDERS表。
我们来考虑应用程序必须执行的一个简单的业务过程。生成订单发票需要完成以下工作:
1.确定适当的查询--应用程序查询ORDERS和LINE_ITEMSGE表搜集生成订单发票所需要的全部信息。可能需要涉及两个表的联接操作,也可能要求每次涉及一个表的多次查询。
2.发出查询--如果没有可用的连接,应用程序初始化一个数据库连接,并直接与数据库驱动程序交互,发出查询。
3.解析查询结果--应用程序读取查询结果中的数据,创建Order和LineItem对象表示这些数据。可能还涉及把数据从数据库格式转换成更符合应用程序语义的格式。
4.打印发票--应用程序使用Order和LineItem遍历所在的订单,计算总价并打印发票。
第一步需要应用程序和数据模型紧密耦合,国为应用程序需要构造查询。类似地,第二步和第三步耦合了应用程序和数据访问代码,在这里就是数据库驱动程序及其资源。
对象/关系映射模式同时去掉了数据模型和数据访问与应用程序及其域对象的耦合。面向对象的概念和关系数据之间的映射成为单个组件的职责,该组件可以独立于应用程序和域对象修改。对象/关系映射通常使用对应程序隐藏其全部映射细节的抽象定义。图3说明了对象/关系映射抽象和实现如何解除耦合:
对象/关系映射实现负责域对象和关系数据之间的映射,通常直接与物理数据库驱动程序交互。一种方法是定义定制的实现,硬编码域对象、应用程序或者系统的全部映射细节。在代码中硬编码细节不容易修改,但与把同样的映射埋藏在应用程序代码或域对象中相比,提供了更清晰的解耦。这种方法也可以作为一种有效的原型策略,在构造功能更完善的解决方案之前开发应用程序。
复杂的对象/关系映射系统使用元数据定义映射的细节。元数据使得实现实现非常通用,因为修改映射的细节不需要升级或重新编译应用程序和域对象。通常映射元数据保存在配置文件或数据库中。同样普遍的做法是定义管理工具,使用户无需要解持久元数据格式也能查看和更新映射细节。
对象/关系映射系统要解决的最基本的问题是,如何从面向对象的概念映射到关系数据库的概念。表1描述了一种通用的类比。这些只是一般的原则,并不是必需的。可以为应用程序使用的每个表定义一个类。应用程序或者对象/关系映射为引用的每个行实例化一个对象实例。每个域对象都使用属性公开类似的列值。
表1 面向对象和关系数据库的一般类比
实际上并不总存在直接的对应关系。比如,对象/关系映射常常隐藏应用程序不需要的关系数据,如单纯用于定义表关系的键或者标识属性。此外对象/关系映射可以代表应用程序进行双向数据转换或者计算属性,甚至封装更多的数据模型细节。如果需要定义的域对象更远地偏离这种类比,必须保证选择的对象/关系映射系统支持要求的映射能力。
一旦定义了对象/关系映射,订单处理程序中生成发票的步骤就很容易描述了:
1.请求订单--应用程序请求相关的订单,对象/关系映射用Order对象集合返回订单。
2.获取订单的每个条目--可以直接在Order对象中取得。要记住在面向对象术语中,Order包含了LineItems集合,并且Order类把该集合作为一个属性公开。
3.计算并报告--像以前那样计算总价并打印发票。
注意,应用程序完全使用域对象操作,没有任何对关系数据模型或数据访问的明确引用。发生的物理数据库操作与前述耦合的方法相同,只不过这一次是由对象/关系映射幕后发出的。
解决像订单处理应用程序所要求的这种简单的情形,实现一个对象/关系映射是完全可以接受的。但是如果准备在应用程序中广泛使用对象/关系映射,你可能不愿意接受这种挑战--从零开始创建一种通用的实现。编写一个高效、通用、元数据驱动的对象/关系映射是一项困难的任务。所幸,有许多商业化的产品和标准可以直接在应用程序中使用。多数这类产品允许使用工具或配置文件定义映射元数据,为物理数据库访问插接不同的数据库驱动程序。
适用性
对象/关系映射模式适用于以下情况:
需要向应用程序逻辑和域对象隐藏物理数据模型和数据访问的复杂性。这样做可以使这些成分更加整洁,集中处理所建模的业务对象和过程。
需要在单个组件中封装域对象映射,以便能够适应数据模型的变化而不必修改应用程序代码或者域对象定义。
需要从域对象映射到多种数据模型而不修改应用程序代码或域对象定义的通用性。这种通用使应用程序能够与多种数据模型结合,不管它们是怎么定义的。
结构
图4说明了一种对象/关系映射实现的静态结构。PersistenceManager接口按照一般的域对象定义定义数据库操作。它通常定义读写和删除与对象的操作。这种接口不需要公开任何数据库细节。也可以把持久性操作集合分散到多个接口,一起形成概念上的PersistenceManager抽象。
ConcretePersistenceManager依据物理数据库操作提供了这些操作的实现。它引用某种形式的Map
data,后者描述了域对象映射。它也使用了PhysicalDatabaseDriver与关系数据库交互。
ConcretePersistenceManager为应用程序封装了数据模型、数据访问和域对象映射。
在对象/关系映射标准和商业对象/关系映射系统中,存在这种结构的几种变体。一种常见的变体是,通过隐式的框架和环境用从PersistenceManager中解耦应用程序代码。其他的变体代替应用程序生成调用的映射代码。
交互
图5说明了应用程序对ConcretePersistenceManager调用读操作时的情形。ConcretePersistenceManager找到描述映射细节的相关元数据,使用这些信息向物理数据库驱动程序 发出操作指令。最后,它使用关系数据和元数据创建一个新的域对象并把这个对象返回给调用者。其他PersistenceManager操作的工作原理类似。
6.效果
对象/关系映射模式有如下效果:
权衡
依赖于另外的商品化产品--多数应用程序都要使用一个商品化的数据库、一个或多个数据库驱动程序和一个应用程序服务器。如果大量地使用对象/关系映射,那么再集成一个商品化的对象/关系映射产品或许也是有利的。购买商品化产品可以显著地降低开发成本。但是重新发布第三方软件确实会带来另外的法律、组装和安装的问题。
优点
清晰的应用程序代码--与包含数据模型和数据访问细节的代码相比,单纯处理域对象的应用程序代码更加清晰,也更容易开发和维护。如果使用定义良好的、只公开逻辑操作的域对象,应用程序的代码就会更集中于它自身的业务逻辑。此外,当以后改变数据模型或数据访问细节时也会处在更有利的位置。
映射到替换的数据模型--最通用的对象/关系映射机制隔离了可配置的映射元数据,可以在不影响应用程序代码的情况下修改元数据。许多对象/关系产品在运行时保存和解释映射元数据,元数据的变化不需要重新编译任何代码。
映射到替换数据模型的概念具有很大的价值。通常数据模型都捆绑到应用程序上。这个特点严重地制约了数据模型的变更,国为需要广泛地升级软件。把数据模型细节封装在元数据中允许改变数据模型,比如重新安排表、转移不同的平台,甚至转移到像基于XML的数据库或者面向对象数据库这样不同类型的数据存储。
元数据也非常有益,可以很快使应用程序变化了的数据模型。从销售和演示的角度看这一点尤其重要,因为可以快速配置应用程序使其处理预期客户的遗留数据。当看到演示是在他们自己的数据环境下进行的时,潜在的客户可能会更感兴趣。
缺点
限制了应用程序对数据访问的控制--应用程序代码只能访问PersisenceManager接口定义的操作。因为ConcretePersistenceManager封装了数据访问的性能。如果采用商业对象/关系映射产品情况就更是如此。
7.策略
如果使用对象/关系映射机制管理域对象,合理的办法是在整个应用程序中使用同样的策略。对于从整体上理解和分析应用程序的数据库交互,一致的数据访问是有好处的。这一节讨论实现对象/关系映射模式中可能遇到的各种问题。
非映射属性
一般而言,参与对象/关系映射实例的域对象对应一个物理数据库表或者联接表中的一行。但是并不是域对象的所有属性都需要保存在数据库中。有时候,它们是根据其他来源计算或转化而成的。这些属性称为非映射属性,因为不是直接映射为关系数据的一部分。非映射属性一般不会引起另外的映射问题,可以用域对象的映射属性自动处理和计算。比如LineItem对象可能公开一个totalPrice非映射属性,根据它的映射属性unitPrice和quantity计算得到。