(一) ORM简介
1.1 ORM的技术背景
对象---关系映射(ORM)是一种为了解决面相对象与关系数据库存在的互不匹配的现象的技术。简单的说,对象---关系映射(ORM)是通过使用描述对象与数据库之间映射的元数据,将面向对象程序自动持久化到关系数据库中。本质上是将数据从一种形式转化到另一种形式,这也同时暗示着额外的执行开销。然而,如果对象---关系映射(ORM)作为一种中间件实现,则会有很多机会做优化,而这些在手写的持久层并不存在。更重要的是用于控制转化的元数据需要提供和管理;但是同样,这些花费要比维护手写的方案要少;而且就算是遵守对象数据管理组织(ODMG)规范的对象数据库依然需要类级别的元数据。
对象---关系映射(ORM)是随着面向对象的软件开发方法发展而产生的。面向对象的开发方法是当今企业级应用开发环境的主流开发方法,关系数据库是企业级应用环境中永久存放数据的主流数据存放系统,对象和关系数据是业务实体的两中表现形式,业务实体在内存中表现为对象,在数据库中表现为关系数据。内存中的对象之间存在关联和继承关系,而在数据库中,关系数据无法直接表达多对多关联和继承关系。因此,对象---关系映射(ORM)系统一般以中间的形式存在,主要实现程序对象到关系数据库数据的映射。
众所周知,面向对象的建模和设计过程的重要指导原则之一就是对象的继承。然而在关系数据库中,缺乏合适,简单的手段来反映这种对象之间的继承关系。阻抗不匹配使软件开发者在描述对象和关系数据库转换时面临的一个关键问题,这也是对象持久化过程中非常难于处理的一个环节。
关系模型把数据组织成行和列,每行表示一条记录,而列则表示为记录中的各种数据项,如果软件系统中数据过于复杂以至于不能用二维的表格来描述,则需要添加附加的表来表示数据之间的“关系”信息。因此,在关系模型中的每一张表都掌握一些数据,而不是全部的数据,而真正的数据可能需要有非常多的记录和数据项来表示。
对象模型中的每个对象都完的整的描述一个特定类的信息,而不拘泥于在行和列中保存数据。表示对象的每一条都是该对象的特定的实例,因此每一个对象都包含了描述自身的所有数据项,与此同时,对象的定义还可以包含一段代码,叫做方法,来进一步诠释对象,而关系模型里面没有这种概念。
对象模型与关系模型之间这种基本的差异导致两种模型的结合并不理想,于是人们试图采用对象数据库来解决两者之间的问题。但是关系数据库技术在现阶段已经发展得相当成熟,占据数据库市场上90%以上的份额,而对象数据库的普及尚需要时日。数据持久层就是要在对象---关系数据库之间提供一个成功企业级别的映射解决方案,尽最大的可能弥补这两种范例之间的差异。
1.2 ORM的基本概
念对象--关系映射(ORM)弥合了所谓的对象关系阻抗不匹配:设计良好的面向对象的模型(基于对现实世界的事物与概念进行建模)与数据库模式中的关系模型(基于数学的数据存储方法)之间的鸿沟。
对象—关系映射(ORM)由持久性工具执行,该工具知道如何查询数据库来检索对象,并知道如何将这些对象持久保存为它们在数据库表和列中的表示,映射在元数据(通常是XML文件)中定义。
1.3 ORM工具及评价标准
采用对象--关系映射(ORM)技术成功与否的关键之处在于对象--关系映射工具自身的好坏. 目前流行的对象—关系映射(ORM)工具有Hibernate,JDO iBATIS
那么如何来判别对象—关系映射(ORM)工具的优劣?可以从以下对象--关系映射(ORM)工具的5个特性来判断:
1.实体映射
开发者将数据库中的实体(一般是一张表)映射为类,对数据库的操作直接转换为对这些实体对象的操作,包括新建,读取,更新,删除(CRUD)等。采用对实体对象的操作,而不是对数据库的直接操作,可以使的数据库访问更方便,并能减少大量不必要的代码。
2.关系映射
数据库中多个表之间会有相互的关系,怎样把这些关系也反映到映射好的实体类中,这就是关系映射。不过相比实体映射,关系映射实现起来更难,这也是评估不同对象—关系映射(ORM)工具好坏的一个重要因素。
3.高级查询
在数据库操作中,用的最多的是数据获取(Retrieve)。数据查询条件复杂,并且查询结果根据不同业务有不同的要求,并不是简单的获取表的所有字段,这也是对象—关系映射(ORM)实现最关键的地方,则使用起来也有难有易
4.事务处理
在数据可靠性要求高的时候,需要引入事务,多数对象—关系映射工具都必须支持事务处理。
5.实体类和操作类的生成
对象—关系映射(ORM)工具说到底就是帮我们生成一个功能强大的数据库访问类,里面包括实体对象类,实体操作类等,为了实现代码的统一及自动化,代码生成的质量也是非常关键的。一个优秀的对象--关系映射(ORM)框工具要搭配一个好的代码生成工具。
(二) Hibernate的体系结构及对象关系映射
2.1 Hibernate的简单描述
Hibernate主要由持久化对象,配置文件和对象—关系映射文件组成,如图:
应用程序 |
Hibernate |
配置文件 Hibernate.cef.xml |
XML映射文件 *.hbm.xnl |
数据库 |
持久化对象(POJO) |
从图中可以看出,应用层抽象出业务的POJO对象并通过Hibernate及映射文件的定义来实现其业务对象的持久化管理。
2.2 基本映射的实现
2.2.1.类名与表名映射
例如:
<class name =””com.hbp.chapter3.model.user” table=””app_user”>
其属性如下:
(1)<class>节点用来声明类名与表名的映射;
(2)name属性指定了映射的类名com.hbp.chapter3.model.user;
(3)table属性指定了映射的数据库表名app_user;
通过此节点配置,Hibernate便可知道类与表的映射关系,也就是对象与关系数据库表的映射。
2.2.2.主键映射
例如:
<id name =”id” column=”id” type=”long” unsaved-value=”null”>
<generator class=”native”/>
</id>
其属性如下:
(1) <id>节点用来声明实体类的标示符(identity)与数据库表主键的对应;
(2) name属性指定了实体类中的主键名称;
(3) column属性指定了对应映射表中的主键字段名;
(4) type属性指定了主键的类别;
(5) <generator>节点指定了主键的生成方式,class属性指定了具体的生成方式。
2.2.3.属性映射
例如:
<property name=”username” column=”username” length=”50”
Type=”string” not-null=”ture” unique=”true”>
</property>
其属性如下:
(1)<property>节点定义了实体类的属性与关系数据库表字段的映射关系;
(2)name属性指定了实体类的属性名称;
(3)column属性指定了对应表名和字段名,如果没有此属性,则默认与name属性值一样;
(4)type属性指定了此字段的数据类型,string表明为字符串的数据类型;
(5)length属性指定了字段的长度;
2.2.4 基本映射中的问题及对策
作为对象---关系映射的纽带,Hibernate映射文件通过将一系列的对象和关系之间不匹配元素进行关联,使之形成一个紧密结合的部分,从而应用实现对关系数据库的解耦。通过对映射定义的了解,可以将Java对象和数据库自动一一关联起来。由于这个映射定义是一个比较复杂的过程,如果选择用手写XML文档的话是非常麻烦和繁琐的,所以通过一些开发工具如XDoclet,可以实现代码与映射之间的同步,并且可以实现项目的持续集成。
2.3 关联映射的实现
在面向对象语言中,关联关系是类与类之间最普通的关系,通过关联关系,使一个类知道另一个类公共的属性和方法。关联关系是有方向的,它们可以使单向的,也可以是双向的。关联关系实现的好坏是评价一个对象---关系映射框架优劣的重要标志。Hibernate对关联关系的实现分为一对一,一对多/多对一和多对多。
2.3.1 一对一关联
一对一关联分为使用主键关联和使用外键关联,在这里我们主要了解使用主键关联。
使用主键关联,关联的两个实体共享同一个主键,如Member(会员)和IDCard(身份证)是一对一的关联,这两个实体共享同一个主键
Memberid使用主键关联,如何让另一个表使用已经生成的主键,也就是关联两个实体如何共享同一个主键,Hibernate的解决方案是使用主键foreign生成机制。
下图使用2个映射文件来表示IDCard与Member之间的关联关系:
Idcard PK,FKL member_id Id_card Validate_date |
Member PK member_id Name sex |
Member.hbp.xml |
Member |
member |
IDCard.hbm.xml |
IDcard |
Idcard |
表,类与映射文件对应关系
Member类映射文件Member.hbm.xml
<hibernate-mapping>
<class name=“com.hbp.chapter4.exl.model.Member”table=“member”catalog=”” hbp”lazy=“false”>
<id name=”memberld” type=“java.lang.string”>
<column name=“member_id” length=”36”/>
<generator class=”guid” ></generator>
</id>
<property name=”name” type=”java.lang.string”>
<column name=”name” length=”10”/>
</property>
<one-to-one name=”idcard” class=”com.hbp.chapter4.exl.model.IDcard” cascade=”all”>
</one-to-one>
</classs>
</hibernate-mapping>
以上映射文件中,使用<one-to-one>节点标示Member与IDCard的一对一关联关系。
IDCard类映射文件IDCard.hbm.xml
<hibernate-mapping>
<class name=“com.hbp.chapter4.exl.model.IDCard”table=“member”catalog=”” hbp”>
<id name=”memberld” type=“java.lang.string”>
<column name=“member_id” length=”36”/>
<generator class=”””foreign” >
<param name=”property”>member</param>
</id>
<property name=””idCard” type=”java.lang.string”>
<column name=”id_card” length=”20”/>
</property>
<property name=”validdateDate” type=”java.util.Date”>
<column name=”validate_Date” length=”10”/>
</property>
<one-to-one name=”member” class=”com.hbp.chapter4.exl.model.Member” constrained=”true”>
</one-to-one>
</class>
</hibernate-mapping>
以上映射文件中节点<generator>的属性class指定为“foreign“,指明使用Hibernate的主键foreign生成机制,而他的子节点<param name=”property”>member</param>指明使用member对象的主键值作为idcard表的主键值。
节点<one-to-one>的属性constrained=”true”指明IDCard应用members的主键作为外键,且idcard与member是强制的一一对应关系。
constrained(约束)表明该类对应的表所对应的数据库表,和被关联的对象所对应的数据库之间,通过一个外键引用对主键进行约束。
2.3.2 一对多关联
一对多关联在实际中应用非常普遍,分为单向关联和多向关联。
在单向关联中只介绍“一对多”关联关系的使用,因为“多对一”会在双向关联使用到。对“一对多”关联关系的实现,Hibernate是在“一”端的映射文件中使用<one-to-many>节点实现的,同时要求“一”端使用集合来存储对应“多”端的对象。一对多单向的映射文件不再详细介绍。
在双向关联中双向关联是由“一对多”和“多对一”两个关联关系组合而成。在双向关联的两端都知道各自的对方。
“一对多”和“多对一”双向关联的映射文件不再详细介绍。
2.3.3 多对多关联
多对多关联是Hibernate中一种较特殊的关联关系,不同于一对一和一对多/多对一的关联关系,它需要借助中间表来完成多对多关联关系信息的保存。多对多只有双向关联,如果只是需要单向关联,那就成了前面介绍一对多或多对一关联关系了。
多对多关联的映射文件不再详细介绍。
2.3.4 关联映射技术的问题及对策
上面介绍了Hibernate的关联关系,从例子中可以看出,Hibernate每次从数据库加载Member对象时,都会同时加载于此Members对象相关联的全部对象,这就需要采用一种数据检索。
Hibernate的数据检索是面向对象的策略,对于不同的数据检索情况,应使用不同的策略,以提高性能。Hibernate的数据检索可以分为立即检索,延迟加载和预先抓取3种策略。
对于多对一和一对一的关联关系应该优先考虑使用预先抓取策略,因为它比立即检索使用的Select语句少,有助于提高性能。而且,如果应用程序希望在加载Members对象时,并不需要同时加载此Member对象的关联对象,则可以使用延迟检索策略。
对于一对多和多对多的关联关系,应该优先考虑使用延迟检索的策略,因为“多”的一端对应的数据可能非常多,使用立即加载或预先抓取的检索策略,回一次性加载大量的数据,但在很多情况下,这些数据并不会被使用到,所以在一对多或多对多关联关系中应避免这种情况。
2.4 继承映射的实现
继承是面向对象语言中的一个基本概念,通过继承,子类可以继承超类的状态和行为,在应用程序中,适用对象继承最多的情况是:创建一个通用的基础类,它的子类通过继承基础类实现不同的行为。
在对象---关系映射框架中,最难持久化的就是一个类的继承结构,因为每一个类的继承结构都有自己独特的需求。
为了解决类继承结构持久化的问题,Hibernate提供了3种继承持久化策略,也就是继承映射策略:
(1) 每个类继承结构一张表;
(2) 每个子类一张表;
(3) 每个具体类一张表。
下图为讨论3中继承映射策略的示例图:
Zoo
Name : string Address: string Id: integer Animals:set |
Animals
Id: long Name: string zoom: Zoom |
Tiger Age: int |
Monkey Kind:string |
面向对象的继承关系树
2.4.1每个类继承结构一张表
这种继承方式只需为继承关系树Animal类映射一个表animal便可。如图表animal包含了Animal类及其子类的所有字段,同时还有一个额外的字段animal_type用于区分Animal类的具体子类。
Zoo类 |
Zoo.hbp.xml |
Zoo表 |
Monkey类 |
Animal类 |
Tiger类 |
Animal.hbp.xml |
Animal表 |
表,类与映射文件之间的关系
Zoo类映射文件Zoo.hbp.xml略写。
Animal类映射文件Animal.hbp.xml如下:
<hibernate-mapping>
<class name=””com.hbp.chapter6.exl.model.Aminal” table=”animal” catalog=”hbp”>
<id name=”id” type=”java.lang.integer”>
<column name=”id”/>
<generator class=”native”></generator>
<id>
<discriminator column=”animal_type” type=”string”/>
<property name=”name” type=”java.lang.string”>
<column name=”name” length=”10”/>
</property>
<many-to-one name=”zoo” column=”zoo_id” class=”com.hbp.chapter6.model.Zoo”/>
<subclass name=”com.hbp.chapter6.exl.model.Tiger”discriminator-value=”Tiger”>
<property name=”age” type=”java.lang.Integer”>
<column name=”age”>
</property>
</subclass>
<subclass name=”com.hbp.chapter6.exl.model.Monkey” discriminator-value=”Monkey”>
<property name”kind” type=”java.lang.string”>
<column name=”kind” length=”10”/>
</property>
</subclass>
</class>
</hibernate-mapping>
在Animal.hbp.xml中,节点discriminator指定animal表中用于区分Animal类型的字段为animal_type,而subclass节点用于映射Animal的子类Tiger类和Monkey类,它通过discriminator-value属性指定当前Animal子类对应的animal_type字段值。Hibernate在运行期间读取animal 表中的数据,会根据animal_type值进行判断,如果animal_type=”Tiger”则映射到Tiger;如果animal_type=”Monkey”,则映射到Monkey类。
2.4.2每个子类一张表
使用这种继承策略,继承树中的每个类和接口都对应一张表,如图:
Zoo类 |
Zoo.hbp.xml |
zoo表 |
Monkey类 |
Animal类 |
Tiger类 |
Animal.hbp.xml |
monkey表 |
Animal表 |
Tiger表 |
表,类和映射文件之间的关系
Zoo类对应的映射文件Zoo.hbp.xml略写。
Animal类对应的映射文件Animal.hbp.xml如下:
<hibernate-mapping>
<class name=””com.hbp.chapter6.exl.model.Aminal” table=”animal” catalog=”hbp”>
<id name=”id” type=”java.lang.integer”>
<column name=”id”/>
<generator class=”native”></generator>
<id>
<property name=”name” type=”java.lang.string”>
<column name=”name” length=”10”/>
</property>
<many-to-one name=”zoo” column=”zoo_id” class=”com.hbp.chapter6.model.Zoo”lazy=”false”/>
<joined-subclass name=”com.hbp.chapter6.ex2.model.Tiger” table=”tiger”>
<key column=”animal_id”/>
<property name=”age” type=”java.lang.Integer”>
<column name=”age”>
</property>
</joined-subclass>
<joined-subclass name=”com.hbp.chaoter6.ex2.model.Monkey” table=”monkey”>
<key column=”animal_id”/>
<property name”kind” type=”java.lang.string”>
<column name=”kind” length=”10”/>
</property>
</joined-subclass>
</class>
</hibernate-mapping>
此映射文件中,joined-subclass节点用于映射Animal类的两个子类Tiger类和Monkey类,它的子节点Key用于指定tiger表和monkey表用于关联animal表的外键字段。
2.4.3每个具体类一张表
使用“每个具体类一张表”继承策略,继承树种的每个具体类对应一张表,如图:
Zoo类 |
Zoo.hbp.xml |
zoo表 |
Monkey类 |
Animal类 |
Tiger类 |
Animal.hbp.xml |
monkey表 |
Tiger表 |
表,类映射文件之间的关系
Zoo类对应的映射文件Zoo.hbp.xml略写。
Animal类对应的映射文件Animal.hbp.xml如下:
<hibernate-mapping>
<class name=””com.hbp.chapter6.exl.model.Aminal” abstract=”true”>
<id name=”id” type=”java.lang.string” column=”id”>
<generator class=”uuid”/>
</id>
<property name=”name” type=”java.lang.string”>
<column name=”name” length=”10”/>
</property>
<many-to-one name=”zoo” column=”zoo_id” class=”com.hbp.chapter6.model.Zoo”lazy=”false”/>
<uuid-subclass name=”com.hbp.chapter6.ex5.model.Tiger” table=”tiger”>
<property name=”age” type=”java.lang.Integer”>
<column name=”age”>
</property>
</union-subclass>
<union-subclass name=”com.hbp.chapter6.ex5.model.Monkey” table=”monkey”>
<property name”kind” type=”java.lang.string”>
<column name=”kind” length=”10”/>
</property>
</union-subclass>
</class>
</hibernate-mapping>
在此配置文件中,union-subclass节点用于映射Animal类的两个子类Tiger和Monkey类。
2.4.4 各种继承映射策略评价及应用对象
1. 每个类继承结构一张表
优点:最简单的继承策略;增加子类很方便;支持多态;因为只有一个表数据存取很快;制作报表非常简单,因为所有数据都在一张表中。
缺点:耦合性高,因为所有类都射到同一个表,继而影响到了其它的类;存在潜在的数据库空间浪费;如果继承类结构很大导致表数据增 长过快。
这种映射一般用于对于一些简单或继承层次比较少的类继承结构,这是一种很好的继承策略。
2. 每个子类一张表
优点:因为采用一对一映射易于理解;支持多态;增加子类很方便,只需增加一个表即可;数据大小呈线性增长。
缺点:每个子类一张表,加上一张超类,会有很多表;读写数据比较耗时,因为需要存取两张表;制作表表比较困难,除非使用视图模拟需要的数据。
这种映射一般用于类之间有交迭。
3. 每个具体类一张表
优点:
制作报表非常简单,应为一个类对应的数据只存放在一张表中;存取单个对象数据使性能很高。
缺点:如果修改一个类,同时需要修改它对应的表及其子类对应的表;不能支持多个角色。
这种映射策略一般适用于类之间没有交迭或者修改类不频繁。
(三)Hibernate性能
3.1 Hibernate的性能担忧
Hibernate的性能是大多数开发者在进行技术选型比较担心的一点,大多数初次尝试使用hibernate的开发者会发现,随着持久对象关系的不断复杂数据的不断增加,hibernate的性能急剧下降。其具体表现为:在设计领域模型时,领域之间的关系会变的非常复杂,充满了一对多,多对一关联关系,于是习惯性的采用了hibernate的one-to-many和many-to-one的关联关系。如果没有特别的设定其映射关系,在查询某个领域对象的几个简单的属性时,它会把所有的关联子对象都取出来,经常在取一个简单属性的时候,将其他并不需要的属性或对象去出来,使得产生非常多不必要的SQL查询。此时如果开启了hibernate显示SQL语句的设置的话,会发现执行一个简单的属性查询hibernate竟然会产生如此多的SQL操作。所以现在的问题是hibernate如何通过最少的SQL查询操作实现它自身提供的功能?它是如何在JDBC基础之上提高性能?
3.2 Hibernate的性能优化
由于hibernate并发线程间并并不共享对象,也不自动的创建实例池,开发者都会认为hibernate的内存利用率是比较低的,在某种程度上来说,这也是事实。但是,在hibernate设计者看来,实例池所带来的好处从一定程度上被程序的编码风格否定了:开发者常常是新建一个对象而不是利用实例池中已有的对象,比如,经常创建一个HashMap对象作为返回结果,或者返回一个组装的String对象。由于开发者不能很容易的确保实例池中的对象不带有状态,并时刻要注意初始化实例池中的对象,与其自己管理状态,不如交由JVM的垃圾收集来自动完成这个工作,这也是hibernate没有采用实例池的一个重要原因。
这样做的好处使得hibernate不需要锁定或同步机制来保证对象的独立性,而对于数据同步上,hibernate则完全依赖于关系数据库处理的并发访问,这一点数据库已经做到非常好了。此外这样做使得hibernate对系统资源的要求变的不是那么高通常能够运行应用服务器和数据库的机器就能够运行hibernate了。而对于特别的对象,如创建非常昂贵的的应用对象,开发者可以实现自己的实例池或缓存池,通过Session.load()方法可以实现状态的装载。
Hibernate内部实现大量地使用了java的反射机制来实现其强大的功能和灵活性。