O/R Mapping 阻抗失衡
本文的目的是以最精炼的语言,理解什么是O/R Mapping,为什么要O/R Mapping,和如何进行O/R Mapping。
什么是O/R Mapping?
广义上,ORM指的是面向对象的对象模型和关系型数据库的数据结构之间的相互转换。
狭义上,ORM可以被认为是,基于关系型数据库的数据存储,实现一个虚拟的面向对象的数据访问接口。理想情况下,基于这样一个面向对象的接口,持久化一个OO对象应该不需要要了解任何关系型数据库存储数据的实现细节。
为什么需要O/R Mapping?
广义上,因为我们需要面向对象来描述我们的业务,我们也需要关系型数据库来存储我们的数据。
有人可能会提到,我们未必要用面向对象来描述业务,或者未必用关系型数据库来存储数据。没错,但是,至少,还是有相当大部分的程序,同时需要这两者的合作。存在即合理。因为同时需要彼此,两者都客观存在,就值得讨论。
既然从广义上,存在即合理,无需讨论为什么需要ORM,很多关于ORM的讨论,其实都是针对上面提到的狭义的定义。但是即使到目前为止,真正能够完美的实现这个狭义定义的ORM的工具,其实还并不存在(很多工具如Hibernate,已经相当接近,但是,离完美还有相当距离)。
既然还不存在,那么,在讨论需不需要之前,我们恐怕要先讨论一下,是否可能,即从理论上,从数学上,面向对象的对象模型和关系型数据库的数据结构之间,到底能不能做到完美的映射?要回答这个问题,我们要解决两个难题。
O/R阻抗失衡
第一个难题,叫“O/R阻抗失衡(O/R Impedance Mismatch)”,指的是OO对象模型和关系型数据库的数据结构之间的,设计理念上的差异。OO的设计理念,是以最正确的语义来描述真实世界;而关系型数据库的设计理念,则是从数学的角度,如何更有效的存储和管理数据。由于两者设计理念的差异,导致它们尽管从数据结构上,可能很相近,但是关注点往往是不同的。
例如:
?从OO的角度,凡是语义明确,每一个对象语义上的属性,就应该定义为一个属性;从关系型数据库的角度,有可能考虑到一些属性从来不会被作为查询条件,而把多个语义上的属性,以一定的格式,存在一个数据表的字段中,也有可能因为一组语义上的属性使用的频度完全不同与另一组属性,即使他们语义上属于一个对象,也有可能将他们拆分成两个数据表来存储。
?从OO的角度,对象只关心自己的固有属性,不需要被唯一标识;但是,从关系型数据库的角度,一般一个数据表中的每一行数据都需要要一个唯一标识,且很可能是语义上没有意义的,如自增长的标识。
?从OO角度的优化,一般遵循SOLID这样的原则,以更正确的语义来组织对象;从关系型数据库角度的优化,往往为了查询性能,来修改字段的类型、长度,修改索引,甚至分表、分库。
?...
一个ORM工具想要通过简单的配置,完美的解决“O/R阻抗失衡”问题,在我看来几乎是不可能的。但是,一定程度上,通过灵活的配置支持绝大多数常见的映射策略,再加上提供可供用户通过自定代码来扩展的接口的话,应该还是可以相当接近完美的。
文化阻抗失衡
第二个难题,叫“文化阻抗失衡(Cultural Impedance Mismatch)”,指的是关系型数据专家和面向对象专家之间的文化差异。关于这个难题的最经典的争论是“到底应该以关系型数据库的数据结构来驱动,还是以OO的对象模型来驱动程序的开发?”。
关于这个争论,OO专家的主要观点是:
我的业务才是程序的核心,数据库只是为我持久化数据的需求服务的,所以设计OO对象模型的时候,我不应该考虑如何存储到关系型数据库的问题;
而数据库专家的主要观点是:
数据才是公司的核心财富,数据的稳定性远远强于OO对象模型的稳定性,由OO对像模型来驱动数据库架构的设计,根本保证不了性能,数据库维护的成本根本不可接受。
其实,争论的真正原因即在于“关系型数据专家和面向对象专家之间的文化差异”。大大牛Scott W. Ambler的文章Why Data Models Shouldn't Drive Object Models(And Vice Versa),很好的解答了这个问题。
归根结底,既然OO和关系型数据库都是不可缺少的部分,需要协同工作,希望做到完美的ORM,不仅仅需要好的ORM工具,更需要无论是OO专家还是数据库专家互相了解对方的技术和设计理念。对一个OO专家,一个ORM工具,可以对其所支持的常见的映射,提供直接的支持,但是,应用这些ORM工具的OO专家,必须了解关系型数据库,知道如何在不影响OO对象的语义的前提下,如何设计一个更容易映射到关系型数据库的对象模型,知道如何选择ORM工具所支持的映射方式,以及如何用自定代码扩展ORM所不能方便支持的映射方式;同样的,对一个关系型数据库的专家,也需要了解OO的语义,和可能发生的重构,从而,使得所设计的数据库结构,更容易映射到OO对象,更容易响应OO对象模型的重构。如此才是真正的和谐,真正的完美。
小结
综上所述,无论对一个OO专家还是对一个关系型数据库专家,都需要了解OO,并了解关系型数据库,了解ORM的基本原理。离开全面的知识,一个再完美的ORM工具也无法被正确使用,拥有这些知识,则能够充分利用已有的ORM工具来加速自己的工作,并且合理的或扩展现有的ORM工具,或用自定代码,实现ORM工具能力之外的ORM映射。这才是理想的ORM实践。下一章节,我们就来谈谈一下,ORM的基本原理。
如何进行O/R Mapping?
简单映射
1. Class <-> Table
一个Class一般可以映射为一个Table,一个Class的实例对应Table的一行数据。但是,一个Table中的每行数据,一般都需要有一个主键来唯一标识这行数据,而一个Class的每个实例,则不一定需要一个唯一标识。
2. Property <-> Field
一个Class的Property一般可以直接映射为Table的一个Field。但是,他们的数据类型不一定直接匹配。如果他们代表的数据类型的语义上可转换,则Field的类型,应大于等于Property的数据类型。如果他们代表的类型语义上不可转换,则需要在应用程序层面,进行自定义的转换。
继承映射
1. 单表映射整个继承体系
用一张数据库表存储整个继承体系中的所有Class的数据,数据表需要额外的标志字段来区分一行记录应该映射到继承体系中的哪一个Class,适合继承体系层次较少,总记录数相对较少,子类对父类的属性扩展也相对不那么频繁的情形。
单表映射整个继承体系的优点是读/写继承体系中的每个Class的数据,都只需操作一张表,性能较好,并且,新增继承类,或扩展Class属性都只需要增减一张表的字段就可以了,易于维护;主要缺点是,因为继承体系中所有的Class共享一张表,表中会有比较多的NULL字段值的数据,浪费了一些存储空间,同时,如果记录数过多,表就会更庞大,也会影响表的读写性能。
2. 一个Class映射一个具体表
所谓一个Class映射一个具体表就是每个Class对应一张数据表,并且,每个数据表冗余包含其父类的所有属性字段,并且,子类和父类共享相同的主键值。一个Class一个具体表方案适合需要较高查询性能,继承体系层次不太复杂,并且基类包含较少的属性而子类扩展较多属性,并且能够承受一定的数据库冗余的情况。
一个Class映射一个具体表方案的优点主要就是查询性能好,读操作只需操作一张表,和实体数据的对应结构清晰,数据库表迁移和维护会比较方便;主要的缺点是数据冗余较大,因为每次插入一条子类数据时,同时要插入一份子类包含的父类字段的数据到所有父类层次表中。
3. 一个Class映射一个扩展表
所谓一个Class映射一个扩展表是指继承体系中的每个Class对应一张数据表,但是,每个子类不冗余包含父类的所有属性,而只是包含扩展的属性和共享的主键值。一个Class映射一个扩展表方案适合继承体系非常复杂,结构易变,并希望最大程度减少数据冗余的情形。
一个Class映射一个扩展表方案的优点是结构灵活,新增子类或插入中间的继承类都很方便,冗余数据最少;但是缺点是,无论读还是写操作都会涉及到子类和所有的父类。读操作时,必须自然链接查询所有的父类对应的数据表,而插入或更新数据时,也需要写所有的父类表。
4. 通用的表结构映射所有的Class
这种方案其实不仅支持用一张表存储一个继承体系,它甚至可以支持,用一张表存储任意数量的不同Class。它的原理是元数据驱动。这张表的每一行,包含一个类型的标识字段,一个表示Class的属性名称的字段和一个表示Class的属性值的字段。在运行时,通过唯一标识取出描述一个Class实例的所有Property的值,再根据Property的名称来映射。
关于各种继承映射的实例分析和详细的优劣比较,请参见:Scott W. Ambler的O/R Mapping In Detail。
关联映射
1. 一对一关联、一对多关联(包含一对一和一对多的自关联)
所谓一对一关联,实际上还可以分为三种情形,即0..1 - 1,1 – 1,1 – 0..1三种情形;而一对多关联则分为* - 1和1 - *。
以下三种方案中第1)种为最常用的映射方案,后面几种是在某些特殊情形下可参考的方案:
1) 最常用的方案为为需要其他对象引用的类对应的表增加一个到被引用对象对应表的外键即可,只不过,与表对应的实体类代码中,对于一对多情形下的“多”这一端,需定义成集合类型;
2) 在Hibernate称为“组件(Component)映射”,举例来说,假如Person类包含一个Address成员类型的属性,而Address由City,Street,ZipCode三个成员属性组成,假如Address除了与Person关联不被其他对象使用,则我们可以考虑只用一张数据表Person来持久化Person和Address这两张表,Person数据表包含Person类中除Address的属性和Address类中的所有属性的集合,当然,这时需要在元数据中特别指明映射关系;
3) 还有一种针对上面的方案中的Person,Address两个类的持久化方案则是将Address类型的所有属性先序列化,再存入Person表的字段Address中,这样也可以只用一张表来持久化两个类,当然,本方案中这种被序列化对象成员数据量应尽量小;
4) 还有一种方案是共享同一主键值的一对一关联。即将原本可以同属于一个表中相对使用不太频繁的字段提出来放在另一张表中,这样,这两张表的记录就可以通过一个相同的主键进行关联。
2. 多对多关联(包含多对多的自关联)
所谓多对多关联自然就是* - *这种情形了。一般都需要一张包含关联双方主键的关联表,在取数据时,需要链接该关联表和数据表。