注:本文只针对设计上采用OOA(面向对象分析)及OOD(面向对象设计)方式,数据存储采用关系型数据库的系统而言。
从对OR-Mapping错误认识谈起
从工作流管理系统和信息共享平台的设计开发过程中,不断的摸索面向对象分析和面向对象设计的方法, 因为面向对象程序设计更加接近人的思维活动,利用这种思想进行程序设计时,可以很大程度的提高编程能力,缩短软件开发周期,减少软件维护的开销。面向对象技术已经成为软件开发的一种趋势。
通过工作流管理系统和信息共享平台的设计和实现,我们对面向对象的程序设计的认识也越来越深刻,逐渐过渡到以面向对象的思想为基准的分层、分布式程序设计开发方式。通过理论、实践、理论的步骤,不断总结学习的过程中,发现原来对OR-Mapping的认识是不正确的。首先概述以下原来软件设计的过程:
首先从需求出发,分析用户需求,界定系统功能,然后做详细设计,详细设计阶段主要是拉通业务流程,根据业务流程,完成数据字典的设计,如果采用面向对象开发的方式,我们将会抽象出业务实体对象,并将他和关系型数据库的表和字段联系起来,进行映射,完成从具有关系型的表到对象的映射,这也就是我开始对OR-Mapping的理解,后来意识到这种认识是错误的,面向对象的开发过程也存在问题。
面向对象的分析设计方法强调分析和设计全部采用面向对象的方法。从用户需求出发,分析面向对象的业务,在分析阶段应该构建出面向对象的分析模型,这些模型是概念模型,和任何数据库或者语言无关。不产生数据字典,而是概念层次的UML图。进入到设计阶段,UML近一步被细化,设计出类图和类接口。这个过程中并不考虑数据库的结构。当把类图和接口设计完毕后,根据设计的对象来生成相应的数据库结构。而不是上面提到的根据数据库结构建立对象,这个由根据“对象来生成相应的数据库结构”的过程其实正是我们要讨论的OR-Mapping的问题,对象关系的映射问题。
下面的章节,将详细讨论OR-Mapping的相关问题,还讨论了面向对象的数据库设计的方式,以及自己对面向对象分析设计的一点感想。
为什么我们要做OR-Mapping?
如果我们采用面向对象的思想进行系统的分析设计,而数据库采用关系型数据库就会存在这个不容忽视的问题。面向对象设计和关系型数据库设计存在很大的不同。对象模型和关系模型两者之间“阻抗不匹配”。对象模型基于软件工程的一些原理,面向对象设计的理论包括封装、关联、聚合、继承、多态,而关系模型主要针对数据的存储,“阻抗不匹配”就成为了他们的主要矛盾。我们希望通过面向对象的设计方式来完成业务流程,这一点可以实现,但最终如何把对象保存下来,是必须考虑的。如果我们的存贮介质采用关系型数据库,那么怎样将对象保存到关系型数据库的时候,矛盾也就出现了。也是我们要做OR-Mapping的主要原因,架起对象和关系型数据库的桥梁。OR-Mapping主要解决对象层次的映射、对象关系的映射以及对象的持久化问题。
OR-Mapping是面向对象分析设计的产物,也是分层设计要解决的问题之一。OR-Mapping会给程序设计带来那些好处呢?在面向对象的分层设计的系统体系中,上层的程序执行最终结果都是要操作数据库,而数据库是关系型,不是面向对象的,正是通过对象关系的映射,使我们实现了只对上层对象的操作实现对表的操作,感觉好象没有数据库的存在,上层只管面向对象编程就可以了。
OR-Mapping的原则
O-R Mapping通常的做法是将程序中的类映射到数据库的一个或多个表, 但是,由这种方式会带来一些问题。首先就是数据实体在数据库和程序中的表现方式不一样,对于一些涉及到多个表的“粗粒度对象”,一个实体类可能会引用到多个其它实体类,也就是说会在涉及到对象粒度的建模方面带来一些问题;其次在同数据库交互时,也涉及到一个转换的问题,如果一个对象涉及到对多个表的操作,问题就更大;最后,当系统做查询操作,需要返回多个对象时,因为涉及到转换的问题,效率就比较低下。
上面提到的这些都是我们实际要考虑的问题,这里就涉及到做O-R Mapping的原则问题。我们做数据映射时依据一个什么样的标准,以什么原则为指导,那些因素是我们在映射的过程中必须考虑的,下面我将谈到这个问题。
①、性能:
对象映射到表时,性能将是最主要的考虑因素。对象映射到表的方式将会对数据库访问的次数有显著的影响。数据库访问通常是在磁盘上或其它外部媒介上执行,它们的访问时间数量级大约在毫秒级,而CPU的处理周期是在纳秒级。因此,较好的做法是浪费一些存储周期和内存空间,来提高慢速IO的性能。其中有一些做法是计算机可以自己做的,例如Cache、DMA方式等等,有一些则需要我们通过编程来手动处理,减少对数据库的访问次数,例如延迟读、为数据置标志位等等。
延迟读的核心思想是用到那些数据就读那些数据,比如一个人员对象,有个部门属性,人员和部门分别存放在不同的表中,在初始化人员这个对象时就没必要把部门这个属性一起初始化,当需要这个属性时在从数据库中读出来,值付给这个属性。
数据标志位的主要目的是为了减少内存和磁盘间的IO开销。一般来说,从数据库读入到内存中的数据,不论有没有修改,最后都需要全部写回到数据库中。这是最简单、最通常,但也是最糟糕的做法。一个100K的数据可能只有1K的数据发生了改动,但却不得不写入99K的额外数据。可以考虑为每一行,甚至每个字段的数据甚至修改位,然后根据修改位的状态来选择性的写回数据库。
②、空间消耗:
不同的映射方式会使数据的存取空间有很大的不同,某些映射不会浪费数据库空间(类似于空值字段),另一些则需要大量无用的数据库记录。空间的消耗将会提高性能。我们是牺牲空间还是要提高性能,可能要因事因时而议,要作到最好的权衡。
③、数据库的可维护性和性能
数据库的可维护性和性能是相互冲突的。在优化数据模型性能的同时,改变应用的维护成本也在上升。也需要我们权衡,取舍。
向上面提到的,O-R Mapping不紧要把对象映射到数据库中,还要把对象的关系映射到数据库中,为了更好的阐述O-R Mapping,先来叙述对象之间的几种不同的关系,以便根据不同的关系来叙述相应的映射规则。
对象之间有以下类型的关系:继承、关联、聚合和组成。要有效地映射这些关系,必须理解它们之间的差异。
继承(Inheritance):继承也称一般化关系,是类与类之间最基本的关系,这里就不在详细阐述。
关联(Associations):关联关系是类与类之间的连接,它使一个类指到另一个类的属性和方法。例如仓库类和产品类之间的关系。
聚合(Aggregation):聚合关系是关联关系的一种,是强的关联关系。聚合是整体和个体之间的关系。例如:汽车类与引擎类、轮胎类之间的关系。关联关系所涉及的两个类是处在同一层次上的,而在聚合关系中,两个类一个代表整体,一个代表部分。
组成(Composition):也成合成关系,组成关系是关联关系的一种,是比聚合关系强的关系。它要求普通的聚合关系中代表整体的对象负责代表部分的对象的生命周期,组成关系是不能共享的。也就是说,代表部分的对象在某一时刻只能与一个对象发生合成关系。
讨论完上面的类之间的关系后下面我们将阐述对象到关系映射的原则。为了更加清晰的说明映射,我们将采用模式语言来叙述。模式是解决某类问题的方法论,我们这里用模式语言来描述,一是为了便于交流,二是为了进行总结,作为解决问题的一种特定方式。
模式语言阐述把对象映射到表时会遇到的三大类问题的解决方案。根据你的项目的需求的不同,你可能会根据灵活性、易于维护性、低数据库空间消耗,以及性能这几个方面来优化映射方法。把对象映射到表的几种模式,请看下面的模式语言图:
下面将详细的阐述每一种模式:
继承映射模式:
把继承层次映射到关系型数据库表有很多种做法。以下展示的模式是单种的映射方式,在实际中,你可以混合使用不同的映射方式。
以下的讨论没有涉及多重继承。在领域层中很少会遇到有多重继承的情况。
模式: One Inheritance Tree One Table
方法:把继承层次中的所有对象的属性全集对应到数据库的单个表中。每条记录中那些无用的字段使用Null值。
结论:
A. 所占空间:就像你在上面的映射中所描述的,存储对象的属性会浪费一些空间,浪费空间的多少取决于继承层次的深度。层次越深,不同的属性越多,属性的全集就越大,也就越浪费空间。
B. 维护成本:当映射比较直接、比较简单的时候,只要继承的层次不会很多,模式的改进相对也会比较直接和简单。
C. 查询:由于映射简单,查询比较容易,只需访问一张表
如下图所示:
模式:One Class One Table
方法:将每个类的属性放到不同的表中。在每个表中插入一个Synthetic OID,将子类的行和父类所在表的行关联起来。
结论:
A. 虚类:注意虚类也需要映射到单独的表中。
B. 所占空间:这种映射几乎有最佳的空间节约性。唯一的容易就是额外的synthetic OID,来连接不同的层级层次。
C. 维护成本:由于这种映射比较直接,也易于了解,模式的改进也相应较为直接、容易。
D. 查询:由于映射需要访问多个表来获取一个对象实例的数据
模式:One Inheritance Path One Table
方法:将每个类的属性映射到不同的表中。并在表中加入该类的父类的所有属性。
结论:
A. 虚类:注意虚类不要映射到表。
B. 所占空间:这种映射提供了最佳的空间占用。没有任何的冗余属性,甚至连额外的synthetic OID都没有。
C. 维护成本:增加或删除父类的属性将会导致对所有子类的表结构的修改。
D. 查询:查询比较相对来说比较容易
模式:Objects in BLOBs
方法:表包括两个字段:一个是synthetic OID,另一个是变长的BLOB,后者囊括了一个对象内的所有数据。使用流把对象的数据存放到BLOB中。
注:
由于这种方式在关系型数据库中用的不够多,如果用这种方式部分也就失去了关系型数据库的意义,这里不再讨论。
关联映射模式:
对于关联关系的对象,主要包括两种类型,一种是1对多,一种是多对多。下面将分别阐述每种类型的关系映射。
模式:Foreign Key Association
方法:该模式展示了怎样把对象间1:m的关系映射到关系型数据库表中。在依赖对象的表中插入所属对象的OID。这个OID可以是数据库的关键字或一个Synthetic Object Identity。
结论:
A. 所占空间:除了在依赖对象表中需要的外键字段以外,没有冗余的字段。
B. 维护成本:较低
C. 查询:相对简单
模式Association Table
方法:该模式展示了怎样把对象间n:m的关系映射到关系型数据库表中。创建一张单独的表,来存放两种对象类型的关系中的对象标志(或外键)。其它的对象类型到表的映射可以使用另外适当的模式来处理。
结论:
A. 所占空间:增加了一张新表,占用了一定的存储空间,但是这是解决多对多问题的较好的方式。
B. 维护成本:较低
C. 查询:相对简单
聚合映射模式:
模式:Single Table Aggregation
方法:把被聚合对象的属性和使用聚合对象的属性放在同一张表中。
结论:
A. 性能:在性能方面,该方案是最佳的,因为只需要访问一张表就能够获取一个带聚合的对象,并读入所有聚合对象。另一方面,由于聚合对象的字段的增多,一次读取将会增大数据库读入的页面数,导致IO带宽的浪费。
B. 维护成本:如果聚合的对象类型被多个对象所引用,那么将会降低可维护性,因为每一次对聚合对象类型的修改都会导致对所有引用聚合对象的修改。
模式:Foreign Key Aggregation
方法:为聚合类型使用单独的表。在表中插入Synthetic Object Identity,并使用使用聚合对象表中的对象标记做一个外键,连接到聚合对象。AggregatingObject(使用聚合对象)映射到一张表,而AggregatedObject(聚合对象)映射到另一张表。聚合对象的表中包含了Synthetic Object Identity。SyntheticOID字段被使用聚合表中的AggregatedObjectsOID外键字段引用。
结论:
A. 性能:Foreign Key Aggregation需要连接操作或至少两次的数据库访问,而Single Table Aggregation只需要一次的数据库操作。如果访问聚合对象的概率较小,这个性能还是可以接受的,如果聚合对象总是需要和使用聚合对象一同返回,那么就需要慎重的思考性能问题了。
B. 可维护性:将对象做分解,例如单独存放AddressType,这将使得维护更加容易,并提高映射的灵活性。
到这里我们将映射模式的准则叙述完毕,下面用一张图来进行总结如下:
各映射模式的对比情况:
面向对象的数据库设计
在这里我们将对面向对象的数据库设计做个说明,因为采用了面向对象的分析设计方法后,这一步和前面的对象关系映射有着密切的联系,其实这一步就是由上面的分析到实现的一个过程。这里主要是阐明面向对象的数据库设计和传统设计的区别和意义。
一般数据库设计方法有两种,即属性主导型和实体主导型。属性主导型从归纳数据库应用的属性出发,在归并属性集合(实体)时维持属性间的函数依赖关系。实体主导型则先从寻找对数据库应用有意义的实体入手,然后通过定义属性来定义实体。
平时我们作的基本属于属性主导型的设计,而通过上面的OR-Mapping后,轻松实现了以实体主导型的面向对象的数据库设计。
从某种意义上讲,是数据库设计的面向对象特征最终奠定了整个系统的面向对象性,其效果归纳如下:
1、数据库结构清晰,便于实现 OOP
由于实现了应用模块对象对数据库对象的完全映射,数据库逻辑模型可以自然且直接地模拟现实世界的实体关系。用户所处的当前物理世界、系统开发者所抽象的系统外部功能,与支持系统功能的内部数据库 (数据结构)一一对应,所以用户、开发者和数据库维护人员可以用一致的语言进行沟通。特别是对多数不了解业务的程序开发人员来说,这种将应用对象与相应的数据对象封装在对象统一体中的设计方法,大大减轻了程序实现的难度,使他们只要知道加工的数据及所需的操作即可,而且应用程序大多雷同,可以多处继承由设计人员抽象出来的、预先开发好的各种物理级超类。
2、数据库对象具有独立性,便于维护
除了数据库表对象与应用模块对象一一对应外,在逻辑对象模型中我们没有设计多重继承的泛化关系,所以这样得到的数据库结构基本上是由父表类和子表类构成的树型层次结构,表类间很少有继承以外的复杂关系,是一个符合局部化原则的结构,从而使数据库表数据破坏的影响控制在局部范围且便于修复,给系统开通后的数据库日常维护工作带来便利。
3、需求变更时程序与数据库重用率高,修改少
在映射应用对象时,除关系映射规范化后可能出现一对多的表映射外,大多数应用对象与表对象是一一对应的。我们可以把规范化处理后的、由一个应用对象映射出来的多个表看成一个数据库对象。因此当部分应用需求变更时,首先,系统修改可以不涉及需求不变更的部分。其次,变更部分的修改可以基本上只限于追加或删除程序模块或追加新库表,而基本上不必修改原有程序代码或原有库表定义,从而大大减少了工作量,降低了工作难度。