65
5 Hibernate 高级特性 在前面的内容中,我们探讨了 Hibernate 的基础使用技术。通过对这些基础技术的把握,我们即 可开始进行基于 Hibernate 的持久层开发。然而,在这些应用技术之后,存在着怎样的运行机制,及 其内部实现方式对应用层可能产生怎样的影响,则是我们下面需要关注的问题。 本章主要分两大部分进行介绍: 1. Hibernate 持久化实现 介绍 Hibernate 对象持久化操作的实现机理。其中包括以下内容: 实体对象生命周期 实体对象识别 数据缓存 事务管理 持久层操作 2. Hibernate 回调与拦截机制 介绍了 Hibernate 中提供的事件捕获和处理机制。其中包括以下内容: Lifecyle Validatable 接口 Hibernate Interceptor 5.1 Hibernate 持久化实现 5.1.1 实体对象生命周期 实体对象的 3 种状态 实体对象的生命周期,是 Hibernate 应用中的一个关键概念。对生命周期的理解和把握,不仅对 Hibernate 的正确应用颇有裨益,而且对 Hibernate 实现原理的探索也极具意义。下面的内容中,我们 就围绕这个主题进行讨论。

Hibernate的高级操作

Embed Size (px)

DESCRIPTION

 

Citation preview

Page 1: Hibernate的高级操作

第 5 章 Hibernate 高级特性

在前面的内容中,我们探讨了 Hibernate 的基础使用技术。通过对这些基础技术的把握,我们即

可开始进行基于 Hibernate 的持久层开发。然而,在这些应用技术之后,存在着怎样的运行机制,及

其内部实现方式对应用层可能产生怎样的影响,则是我们下面需要关注的问题。

本章主要分两大部分进行介绍:

1. Hibernate 持久化实现

介绍 Hibernate 对象持久化操作的实现机理。其中包括以下内容:

实体对象生命周期

实体对象识别

数据缓存

事务管理

持久层操作

2. Hibernate 回调与拦截机制

介绍了 Hibernate 中提供的事件捕获和处理机制。其中包括以下内容:

Lifecyle 与 Validatable 接口

Hibernate Interceptor

5.1 Hibernate 持久化实现

5.1.1 实体对象生命周期

实体对象的 3 种状态

实体对象的生命周期,是 Hibernate 应用中的一个关键概念。对生命周期的理解和把握,不仅对

Hibernate 的正确应用颇有裨益,而且对 Hibernate 实现原理的探索也极具意义。下面的内容中,我们

就围绕这个主题进行讨论。

Page 2: Hibernate的高级操作

这里的实体对象,特指 Hibernate O/R 映射关系中的域对象(即 O/R 中的“O”)。

实体对象生命周期中的 3 种状态(考虑到中英术语意译上可能的语义丢失,下面的内容中,我们

将直接引用英文术语进行描述):

1. Transient(自由状态)

所谓 Transient,即实体对象在内存中的自由存在,它与数据库中的记录无关。如: public void methodA{

TUser user = new TUser(); user.setName("Emma");

}

这里的 user 对象,与数据库中的记录没有任何关联。

2. Persistent(持久状态)

何谓 Persistent? 即实体对象处于由 Hibernate 框架所管理的状态。这种状态下,实体对象的引用

被纳入 Hibernate 实体容器中加以管理。

处于 Persistent 状态的对象,其变更将由 Hibernate 固化到数据库中。

看下面的例子:

TUser user = new TUser(); TUser anotherUser = new TUser(); user.setName("Emma"); anotherUser.setName("Kevin"); //此时user和anotherUser都处于Transient状态 Transaction tx = session.beginTransaction(); session.save(user); //此时的user对象已经由Hibernate纳入实体管理容器,处于Persistent状态 //而anotherUser仍然处于Transient状态 tx.commit(); //事务提交之后,库表中已经插入一条用户"Emma "的记录 //对于anotherUser则无任何操作 Transaction tx2 = session.beginTransaction(); user.setName("Emma_1"); //Persistent anotherUser.setName("Kevin_1");//Transient tx2.commit(); //虽然这个事务中我们没有显式调用Session.save方法保存user对象 //但是由于处于Persistent状态的对象将自动被固化到数据库中,因此user //对象的变化也将被同步到数据库中 //也就是说数据库中"Emma"的用户记录已经被更新为"Emma_1" //此时anotherUser仍然是个普通Java对象,处于Transient状态,它不受 //Hibernate框架管理,因此其属性的更改也不会对数据库产生任何影响

可以看到,处于Transient状态的实体对象,可以通过Session.save方法转换为Persistent状态。而同

样,如果一个实体对象是由Hibernate加载(如通过Session.load方法获得),那么,它也处于Persistent

状态。如:

//由Hibernate返回的Persistent对象 TUser user = (TUser)session.load(TUser.class,new Integer(1)); //Session.load方法中,在返回对象之前,Hibernate就已经将此对象纳入其 //实体容器中,这里的user对象即处于Persistent状态

Persistent对象对应了数据库中的一条记录,可以看作是数据库记录的对象化操作接口,其状态的

变更将对数据库中的记录产生影响。

简而言之,如果一个实体对象与某个Session实例发生了关联,并处于对应Session的有效期内,那

Page 3: Hibernate的高级操作

么它就处于Persistent状态。

3. Detached(游离状态)

处于 Persistent 状态的对象,其对应的 Session 实例关闭之后,那么,此对象就处于“Detached”

状态。

Session 实例可以看作是 Persistent 对象的宿主,一旦此宿主失效,那么其从属的 Persistent 对象即

进入 Detached 状态。

我们来看以下实例:

TUser user = new TUser(); user.setName("Emma"); //此时user处于Transient状态 Transaction tx = session.beginTransaction(); session.save(user); //此时的user对象已经由Hibernate纳入管理容器,处于Persistent状态 tx.commit(); session.close(); //user对象此时状态为Detached,因为与其关联的session已经关闭

上面的例子中,user 对象从 Persistent 状态转变为 Detached 状态。那么,这里的 Detached 状态与

Transient 状态有什么区别?

区别就在于 Detached 对象可以再次与某个 Session 实例相关联而成为 Persistent 对象: TUser user = new TUser(); user.setName("Emma"); //此时user处于Transient状态 Transaction tx = session.beginTransaction(); session.save(user); //此时的user对象已经由Hibernate纳入管理容器,处于Persistent状态 tx.commit(); session.close(); //user对象此时状态为Detached,因为与其关联的session已经关闭 Transaction tx2 = session2.beginTransaction(); session2.update(user); //此时处于Detached状态的user对象再次借助session2由Hibernate纳入管 //理容器,恢复Persistent状态 user.setName("Emma_1"); //由于user对象再次处于Persistent状态,因此其属性变更将自动由 //Hibernate固化到数据库中 tx2.commit();

可以看到,这里我们通过 Session.update方法将Detached对象再次与Hibernate持久层容器相关联,

因而 user 对象又转变为 Persistent 状态。

一个很自然的问题。

这个 Detached 状态的 user 对象,与 初的 Transient 状态的 user 对象到底有何区别?

既然 Detached 状态的 user 对象已经与 Hibernate 实体容器无关,那么这两者还有什么差异?

回顾上面的代码。

通过如下代码,我们创建了 Transient 状态的 user 对象:

TUser user = new TUser(); user.setName("Emma");

而借助 session.save 方法,我们将其转变为 Persistent 状态。

Page 4: Hibernate的高级操作

session.save(user);

关键就在这里,在 session.save 方法执行过程中,user 对象的内容已经发生了改变。

在创建 Transient 对象时,我们为 user 对象设定了一个 name 属性。此时 user 对象所包含的数据

信息也仅限于此,它与数据库中的记录并不存在对应关系。

而 Session.save 执行之后,Hibernate 对 user 对象进行了持久化,并为其赋予了主键值。在这里,

也就是 user.id 属性(回顾之前的示例场景,TUser 类中,id 属性被设定为自增型主键),由于 id 属性

是主键,可以惟一确定库表中的一条记录,那么,这个 user 对象自然就可以与库表中具备相同 id 值

的记录相关联。

这就是前后两个状态中,user 对象之间的基本差异,Transient 状态的 user 对象与库表中的数据缺

乏对应关系,而 Detached 状态的 user 对象,却在库表中存在相对应的记录(由主键惟一确定),只不

过由于 Detached 对象脱离了 Session 这个数据操作平台,其状态的变化无法更新到库表中的对应记录。

就目前这个示例,简而言之,Transient 状态中的实体对象,无主键信息,而 Detached 状态的实

体对象,包含了其对应数据库记录的主键值。

我们也可以人工制造一个 Detached 状态对象: Tuser user = new Tuser(); user.setName("Emma"); //硬编码为其指定主键值(假设库表中存在id=1的记录) user.setId(new Integer(1)); //此时user对象成为一个“人造detached对象” Transaction tx= session.beginTransaction(); session.update(user); //Session根据其主键值,将其转变为Persistent状态 user.setAge(new Integer(20)); tx.commit(); //观察数据库中的记录,发现Age字段的值已经发生变化。 session.close();

对于这里的简单示例,我们可以通过编码将一个 Transient 状态的对象手动的与库表记录形成关

联,使其转变为一个 Detached 状态的对象,此时,我们手工构造的这个 Detached 对象与通过 Session

构造的 Detached 对象并没有什么区别。

不过,考虑到实际情况可能并非这么简单,Hibernate 在判定对象处于 Detached 状态还是 Transient

状态时,有着更加复杂的机制。

判定一个对象是否处于 Transient 状态的条件:

1. 首先,对象的 id 属性(如果此属性存在的话)是否为 null。

对于上面的示例,Hibernate 即根据此条件进行判定。

2. 如果指定了 id 属性的 unsaved-value(请参见稍后对 unsaved-value 的讨论),那么 id 属性是

否等于 unsaved-value。

3. 如果配备了 Version 属性(参见稍后的“事务管理-乐观锁”部分描述),version 属性是否

为 null。

4. 如果配备了 Version 属性,且为 vesion 指定了 unsaved-value,version 属性值是否等于

unsave-value。

5. 如果存在 Interceptor(参见稍后的“Hibernate 回调与拦截机制”部分内容),那么

Interceptor.isUnsaved 方法是否返回 true。

Page 5: Hibernate的高级操作

相对与 Persistent 状态与 Detached 状态的转变。实体对象从 Persistent 状态转变为 Transient 状态,

一般由 session.delete 方法完成: Tuser user = new Tuser(); user.setName("Emma"); Transaction tx= session.beginTransaction(); session.save(user);//Transient => Persisent tx.commit(); Transaction tx2= session.beginTransaction(); session.delete(user); tx2.commit();//Persistent => Transient System.out.println(user.getId());//打印user.id属性值

通过 session.delete 方法, Persistent 状态的 user 对象转变为 Transient 状态。

代码 后我们打印出了其 id 值,可以看到,这个 id 值非 null,这是否意味着 user 对象是处于

Detached 状态?

这里再次重复一下 Detached 状态与 Transient 状态之间的差异,Transient 状态的实体对象与库表

中的记录无关,我们无法根据 Transient 对象中的信息在库表中寻找到对应的记录,而 Detached 状态

的对象,虽然与 Session 实例脱离,但我们根据其中的信息,能够寻找到库表中对应的数据记录。

而这里,这个 id 所对应的库表记录已经删除,此时的 user 对象与库表中的记录已经不存在对应

关系,因此,它处于 Transient 状态。

VO 与 PO

有时候,为了方便,我们也将处于 Transient 和 Detached 状态的对象统称为值对象(VO 即 Value

Object),而将处于 Persistent 状态的对象称为持久对象(PO 即 Persistence Object)。

这是站在“实体对象是否被纳入 Hibernate 实体管理容器”的立场加以区分的,非管理的实体对

象统称 VO,而被管理的实体对象称为 PO。

再从 VO 和 PO 的角度重复一下上面的描述:

VO 和 PO 的主要区别在于:

1. VO是相对独立的实体对象,处于非管理状态。

2. PO是由Hibernate纳入其实体管理容器(Entity Map)的对象,它代表了与数据库中某条记录对

应的Hibernate实体,PO的变化在事务提交时将反映到实际数据库中。

3. 如果一个PO与其对应的Session实例分离,那么此时,它又会变成一个VO。

由 PO、VO 的概念,又引申出一些系统层次设计方面的问题。如在传统的 MVC 架构中,位于

Model 层的 PO,是否允许被传递到其他层面。由于 PO 的更新 终将被映射到实际数据库中,如果

PO 在其他层面(如 View 层)发生了变动,那么可能会对 Model 层造成意想不到的破坏。

因此,一般而言,应该避免直接将 PO 传递到系统中的其他层面,一种解决办法是,通过构造一

个新的 VO,通过属性复制使其具备与 PO 相同的属性值,并以其为传输媒质(实际上,这个 VO 被

用作 Data Transfer Object,即所谓的 DTO),将此 VO 传递给其他层面以实现必须的数据传送。

属 性 复 制 可 以 通 过 Apache Jakarta Commons Beanutils ( http://jakarta.apache.org/

commons/beanutils/)组件提供的属性批量复制功能,避免繁复的 get/set 操作。

Page 6: Hibernate的高级操作

下面的例子中,我们把 user 对象的所有属性复制到 anotherUser 对象中:

TUser user = new TUser(); TUser anotherUser = new TUser(); user.setName("Emma"); user.setAge(new Integer(1)); try { BeanUtils.copyProperties(anotherUser,user); System.out.println("UserName => " +anotherUser.getName() ); System.out.println("User Age => " + anotherUser.getAge() ); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); }

5.1.2 实体对象识别

实体身份识别(Data Identity)

在 Java 语言中,对象之间的比较主要通过以下两种方式:

1. 引用比较(==)

引用比较的作用是判断两个变量是否引用了同一个对象实例。如 TUser user1 = new TUser(); TUser user2 =user1; if (user1 == user2){ …… }

2. 内容比较

String string1="string1";

String string2="string2";

if (string1.equals(string2)){

……

}

内容比较的目的是为了判定两个对象所包含的数据是否相同。

以上两种方式是 Java 语言中对象比较的基本方式,基于这种机制,我们可以很方便地分辨对象

之间的差异。而这里,面对持久层逻辑,我们必须面对新的问题:如何判定两个实体对象是否相等。

假设出现这样的情况: TUser user1=(TUser)session.load(TUser.class,new Integer(1)); user1.setAge(new Integer(21)); TUser user2=(TUser)session2.load(TUser.class,new Integer(1));

上面的示例中,user1 和 user2 这两个对象是否相等?

从 Java 语言规范的角度而言,这两个对象无论是引用,还是具体内容都不相同。但是,站在持

Page 7: Hibernate的高级操作

久层角度而言,这两个对象却都代表着数据库中的同一条记录(t_user 表中 id 为 1 的记录),具备等

价的含义。这种等价关系,是由于持久层逻辑的出现而引入的,而这也同时引出了下面我们所要探讨

的主题:实体对象的身份识别。

如何确定一个实体对象的身份?站在数据库的角度,我们认为,在一个库表结构中,主键可以惟

一确定一条记录,那么,对于拥有同样主键值的实体对象,则认为他们等同。

如上面的例子中,id 是 t_user 表的主键,对于两个 TUser 对象,只要其主键值相同,我们则认为

他们等同。

对于 Hibernate 而言,这个规则也成立。net.sf.hibernate.engine.Key 类(Hibernate 3 中对应类为

org.hibernate.engine.Key)封装了 Hibernate 用于区分两个实体对象的识别信息。

图 5-1 是一个 Key 对象的运行期内存快照。

图 5-1 Key 对象的运行期内存快照

可以看到,Key 中主要维持了 3 个属性,实体类,实体类名和实体 ID。通过实体类名和 ID,Hibernate

即可确定这个实体在数据库中的对应库表和记录,从而将其与其他对应不同记录的实体对象区分开

来。

另外,Key 在 Hibernate 缓存中也扮演着数据标识的角色,Hibernate 将根据 Key 在缓存中寻找是

否有对应的数据存在。

于此同时,在持久层之外,对象是否等价在业务逻辑层可能还有另外的含义,往往存在一些特定

的数据实体判定规则。

如,对于 t_user 表中两条不同的记录。其 name 字段相同,那么我们就认为,这两条记录实际上

对应着同一个人,从这个角度上来看,这两个数据实体等价。此时我们用作判定的条件,既非对象引

用、对象内容,也非类名和 ID,而是特定领域中的逻辑规则。

这样的逻辑规则如何体现在我们的实体对象之间?比较自然的方法是通过覆盖 Object.equals 方

法来实现。

Equals 和 hashCode 方法

Java Collection 将通过 hashCode/equals 方法判定两个对象是否相等。

我们知道,Set 集合类型不允许集合中出现两个相同对象。如: Set set = new HashSet(); TUser user1 = new TUser(); TUser user2 = new TUser(); user1.setId(new Integer(1)); user2.setId(new Integer(1)); set.add(user1); set.add(user2);

System.out.println("Items in set =>"+set.size());

Page 8: Hibernate的高级操作

观察屏幕输出:Items in set =>2;

这里的 TUser 对象并没有覆盖 Object.equals/hashCode 方法,因此 Collection 将调用 TUser 的父类

(也就是 Object)的 equals/hashCode 方法判断这两个对象是否相等: public boolean equals(Object obj) { return (this == obj); }

Object.equals 方法只是简单对比了两个对象的引用是否相等,显然,这里 user1==user2 并不成立,

于是 Collection 判定这两个对象互不相等,将其分别纳入集合中。

现在,我们修改一下 TUser 类,使之覆盖 Object.equals 和 Object.hashCode 方法: public class TUser implements Serializable { …… public boolean equals(Object object) { TUser usr = (TUser) object; return this.getId().equals(usr.getId()); } public int hashCode() { return this.getId().intValue(); } }

再次运行之前的测试代码,我们看到输出:Items in set =>1;

Set 集合认为 user1 和 user2 是两个相同的对象,因此,只在集合中维持了一个实例 user1。

Collection 在判断两个对象是否相等的时候,会首先调用对象的 hashCode 方法,如果 hashCode

相同的话,随即调用其 equals 方法,如果两次判断均为真,则认为对比的两个对象相等。

其实上面的现象,在 Java 日常开发中我们也可能经常碰到,感兴趣的读者可以看看 java.lang.Integer

类的 hashCode 和 equals 方法实现,再尝试一下在 Set 中重复添加 intValue=1 的 Integer 对象观察结果。

对于我们上面这个改造后的 TUser 类而言,无论其实例之间其他属性取值有怎样的差异,只要其

id 相等,则 Set 集合中只会维持相同 id 的一个实例。

那么,这对 Hibernate 又意味着什么?

如果我们在 MiddleGen 的 OR 映射选项里选择了 Generate Equals/HashCode,那么通过 hbm2java

生成 后的代码时,我们会发现代码中添加了如下的 Equals/hashCode 方法,以 TAddress 类为例: public boolean equals(Object other) { if ( (this == other ) ) return true; if ( !(other instanceof TAddress) ) return false; TAddress castOther = (TAddress) other; return new EqualsBuilder() .append(this.getId(), castOther.getId()) .isEquals(); } public int hashCode() { return new HashCodeBuilder() .append(getId()) .toHashCode(); }

这两个方法的原理其实跟之前我们手工编码的实现原理类似:如果 id 相同,则认为对比的两个

对象相同。这实际上也符合我们之前讨论的持久层的实体对象身份识别原则。不过,这里却产生了另

外一个问题。

再尝试运行以下代码: TUser user = (TUser)session.load(TUser.class,new Integer(2));

Page 9: Hibernate的高级操作

TAddress addr1 = new TAddress(); addr1.setAddress("Shanghai"); TAddress addr2 = new TAddress(); addr2.setAddress("Guangdong"); user.getAddresses().add(addr1);//addr1.id==null user.getAddresses().add(addr2);//addr2.id==null System.out.println("Items in set =>"+user.getAddresses().size());

从代码逻辑上来看,其目的显然是想为 user 对象添加两个关联的地址对象。

但是,运行此代码,我们却发现输出是:Items in set =>1

也就是说,addr2 并没有真正被加入到 user.addresses 集合中去。这样,我们在稍后调用

session.save(user)对象时,也只会向数据库插入一条地址数据。

为什么出现这样的现象?原因就在于主键值的生成机制,对于这里的 TAddress 对象,我们采用

了“identity”的 id 生成方式,其 id 只有在 Session.save 方法执行之后才会被设置。

我们在向 user.addresses 集合中添加对象时,Session.save 方法尚未执行,因此,id==null,参照上

面 equals/hashCode 的实现机制,只要 id 相同(现在 id 都等于 null),则认为这两个对象相同,因此只

在集合中维持了 addr1,而没有将 addr2 添加进去。

那么,我们是否应该在实体类中覆盖 equals/hashCode 方法?如果应该覆盖的话,采取怎样的

equals/hashCode 实现比较合适?

首先,我们来看在不覆盖 equals/hashCode 方法的情况下,可能出现什么问题。

分析以下代码: TUser user = (TUser)session.load(TUser.class,new Integer(1)); System.out.println("Address count=> "+user.getAddresses().size()); Iterator it = user.getAddresses().iterator(); TAddress addr = (TAddress)it.next(); //////////////////////////////////////////////////////////// TUser user2 = (TUser)session2.load(TUser.class,new Integer(1)); user2.getAddresses().add(addr); System.out.println("Address count=> "+user2.getAddresses().size()); Transaction tx = session2.beginTransaction(); session2.save(user2); tx.commit();

代码用反斜杠分隔为两部分,第一部分我们加载了一个 id=1 的 User 对象,并获得了它所关联的

一个 TAddress 对象 addr(假设此 user 有 3 个关联 address 对象)。

第 2 部分,我们通过另外一个 Session 再次加载了 id=1 的 user 对象,并将前面的 addr 对象加入

其中。

注意,当 id=1 的 user 对象加载的时候,其 addresses 属性已经包含了所关联的 3 条记录,这样,

由于 addr 对象的引用与 user.addresses 中的 3 个对象都不相同,addr 成功加入。

这就导致了一个问题,user.addresses 中包含了两个针对同一库表记录的实体。

这样,执行 session2.save 方法时,我们将得到一个 NonUniqueObjectException 异常。

观察屏幕输出: Address count=> 3

Page 10: Hibernate的高级操作

Address count=> 4 net.sf.hibernate.NonUniqueObjectException: …… ……

这就是在不覆盖 equals/hashCode 方法的情况下我们所面对的问题:实体对象的跨 Session 识别。

假设上面的代码中只使用了一个 session 实例,那么第二次加载 user 对象及其关联的 address 时,

addresses 集合中的 3 个 address 对象实际上与第一次加载的 3 个完全一样(Session 在第一次加载时将

这些数据在内部进行了缓存,第二次直接返回这些缓存中的实例引用)。这样在添加 addr 时,集合会

将其判定为已存在元素从而维持不变。这样就不会出现 NonUniqueObjectException 异常。

我们的实际开发中,这种情况可能比较少见,如果确定系统中不会出现类似的冲突,那么我们可

以不必覆盖 equals/hashCode 方法。

但是,如果系统中可能出现类似的冲突,该如何面对?

一个方法是实现所谓的值比对,即在 equals/hashCode 方法中,对实体类的所有属性值进行比对,

如果两个实体类的属性值都一致,那么判定为相等,否则反之。

如: public boolean equals(Object object) { if (!(object instanceof TAddress)) { return false; } TAddress rhs = (TAddress) object; return new EqualsBuilder() .appendSuper(super.equals(object)) .append(this.userId, rhs.userId) .append(this.type, rhs.type) .append(this.idx, rhs.idx) .append(this.address, rhs.address) .append(this.tel, rhs.tel) .append(this.zipcode, rhs.zipcode) .append(this.id, rhs.id) .isEquals(); } public int hashCode() { return new HashCodeBuilder(-599736627, 1187168773) .appendSuper(super.hashCode()) .append(this.userId) .append(this.type) .append(this.idx) .append(this.address) .append(this.tel) .append(this.zipcode) .append(this.id) .toHashCode(); }

上面的 equals/hashCode 方法将实体类的所有属性都纳入了运算,实现了“值比对”机制。

为每个类都编写如上的代码无疑是件苦差使,好在现在已经有了许多辅助工具来帮我们自动完成

上 面 的 工 作 , 如 Intellij IDEA 中 已 经 内 置 了 equals/hashCode 方 法 的 自 动 生 成 功 能

(Code->Generate->equals&hashCode 菜单),而 Eclipse 中也有对应的免费插件可以使用:

a) Commonclipse(http://commonclipse.sf.net)

上面的 equals/hashCode 方法就是由 Commonclipse 插件自动生成。

b) Commons4E(http://commons4e.berlios.de/)

另外注意,使用“值比对”方法只需针对实体类的属性进行处理,而不要涉及实体类所关联的集

Page 11: Hibernate的高级操作

合类的比对,否则在多对多关系中很容易引发一些其他的问题。

值比对的缺点在于检查过于严格,属性稍有差异,对象即被判定为不等,在某些情况下,这样的

策略并不适用。

之前我们曾经讨论过业务逻辑层中的对象判定问题,除了“值比对”,还有另外一种基于业务逻

辑的对象判定方式“业务关键信息判定”。

业务关键信息判定实际上是值比对的一个子集,也就是说,在进行实体属性比对的时候,我们只

对一些业务关键属性进行判断,如之前讨论业务逻辑层的对象判定时所提及的例子:如果两个 user

对象的 name 属性相等,则判定为等同。如: public boolean equals(Object object) { if (!(object instanceof TUser)) { return false; } TUser rhs = (TUser) object; return new EqualsBuilder() .appendSuper(super.equals(object)) .append(this.name, rhs.name) .isEquals(); } public int hashCode() { return this.name.hashCode(); }

这种方式需要针对业务逻辑进行判定,因此需要特别小心。如对上面的例子,我们在实施此判定

策略之前必须保证以下前提:t_user 表中不可能出现 name 相同的记录(name 为逻辑主键)。

脏数据检查

何谓脏数据(Dirty Data)?

这里的“脏”可能有些误导,脏数据并非废弃或者无用的数据,而是指一个数据对象所携带的信

息发生了改变之后的状态。

如我们从数据库中读取了一个 TUser 对象: Transaction tx = session.beginTransaction(); TUser user = (TUser)session.load(TUser.class,new Integer(1)); //此时user对象处于由数据库读出的原始状态 user.setAge(30); //此时user对象所携带的信息发生了变化,成为所谓的“脏数据” tx.commit();

事务提交时,Hibernate 会对 session 中的 PO 进行检测,判断那些发生了变化,并将发生变化的

数据更新到数据库中。

这里就存在一个问题,Hibernate 如何判断一个数据对象是否发生了改变,或者说,Hibernate 如

何进行脏数据识别?

脏数据检查的一般策略大致有下面两种:

1. 数据对象监控

数据对象监控的实现方式,大体上是通过拦截器对数据对象的设值方法(setter)进行拦截,

拦截器的实现可以借助 Dynamic Proxy1或者 CGlib 实现。一旦数据对象的设置方法被调用(通

1 参见第1部分中关于Dynamic Proxy模式的描述。

Page 12: Hibernate的高级操作

常这也就意味着数据对象的内容发生变化),则将其标志为“待更新”状态,之后在数据库

操作时将其更新到对应的库表。

2. 数据版本比对

在持久层框架中维持数据对象的 近读取版本,当数据提交时将提交数据与此版本进行比

对,如果发生变化则将其同步到数据库相应的库表。

Hibernate 采取的是第二种检查策略。

结合一个实例,我们来探讨一下 Hibernate 脏数据检查的具体实现: TUser user = (TUser) session.load(TUser.class, new Integer(1)); Transaction tx = session.beginTransaction(); user.setName("Kevin"); tx.commit();

1. 首先,我们通过 Hibernate 加载 id=1 的 user 对象:

TUser user = (TUser) session.load(TUser.class, new Integer(1));

假设此时 user.name 属性值为 “Emma”。

2. 启动事务

Transaction tx = session.beginTransaction();

3. 调用 user 的设值方法,将其 name 属性修改为“Kevin”

user.setName("Kevin");

4. 事务提交,好戏开场

tx.commit();

Transaction.Commit 方法随即调用 Session.flush: public void commit() throws HibernateException { …… if ( session.getFlushMode()!=FlushMode.NEVER )

session.flush(); …… }

Session.flush()方法中,会完成两个主要任务: public void flush() throws HibernateException { …… flushEverything();//刷新所有数据 execute();//执行数据库SQL完成持久化动作 …… }

flushEverything 会首先完成一些预处理工作(如调用对应的 interceptor、协同级联关系等);之后,

即调用 flushEntities 方法对当前 Session 中的实体对象进行刷新,而这个过程,也是脏数据判定的关键。

在继续下面的过程探讨之前,我们首先来看一个内部数据结构“EntityEntry”: static final class EntityEntry implements Serializable { LockMode lockMode; //当前加锁模式 Status status;// 当前状态[Loaded,Deleted,Loading,Saving…] Serializable id; Object[] loadedState; //实体最近一次与数据库的同步版本 Object[] deletedState; //实体最近一次删除时的版本 boolean existsInDatabase; Object version; //版本号[用于乐观锁,请参见事务管理部分]

Page 13: Hibernate的高级操作

// ClassPersister是针对实体类的持久化封装,通过它我们可以获得实体类 //属性对应的数据库字段类型等信息,或者执行对应的持久化操作(如insert、 //update) transient ClassPersister persister; String className; boolean isBeingReplicated; EntityEntry( Status status, Object[] loadedState, Serializable id, Object version, LockMode lockMode, boolean existsInDatabase, ClassPersister persister, boolean disableVersionIncrement ) { this.status = status; this.loadedState = loadedState; this.id = id; this.existsInDatabase = existsInDatabase; this.version = version; this.lockMode = lockMode; this.isBeingReplicated = disableVersionIncrement; this.persister = persister; if (persister!=null) className = persister.getClassName(); } }

EntityEntry 是从属于 SessionImpl(SessionImpl 是 Session 接口的实现)的一个内部类,每个

EntityEntry 对应一个实体类实例,保存了该实体类的状态信息,如其 近一次与数据库同步时的版本

(loadedState)等。

为了更加形象化地理解,下面给出了本例在运行期间,user 对象对应的 EntityEntry 内存快照(图

5-2)。

图 5-2 user 对象对应的 EntityEntry 内存快照

可以看到, EntityEntry 中包含了对应实体对象的所有状态信息,特别是在其 loadedState 属性中,

保存了实体对象 近一次与数据库同步的版本副本。

前面说过,Hibernate 实现脏数据检查机制是基于数据版本比对机制,而这也就是 Hibernate 实现

脏数据判定的原始依据。

在 Session 中,保存了所有与当前 Session 实例相关联的实体对象的当前实例和原始状态信息(即

Page 14: Hibernate的高级操作

EntityEntry)。这两者以“key-value”的形式,保存在 SessionImpl.entityEntries 数据结构中。

SessionImpl.entityEntries 是一个 Map 型的数据结构,其中每个项目(Entry)都包含了当前与 Session

关联的一个实体对象实例及其原始信息。以实体对象为 Key,而以对应的 EntityEntry 为 Value。

Session.flushEntities 方法的工作,就是遍历 entityEntries,并将其中的实体对象与其原始版本进行

比对,判断对象状态是否更改。 private void flushEntities() throws HibernateException { //将Map型的entityEntries转换为List,用于循环遍历 List list = IdentityMap.concurrentEntries(entityEntries); int size = list.size(); for ( int i=0; i<size; i++ ) { Map.Entry me = (Map.Entry) list.get(i);

//取出Entry.Value中的EntityEntry EntityEntry entry = (EntityEntry) me.getValue(); Status status = entry.status;

//判断实体当前状态 if (status!=LOADING && status!=GONE)

//取出Entry.Key中保存的当前实体对象,连同EntityEntry //交由flushEntity方法进行刷新处理 flushEntity( me.getKey(), entry );

} }

flushEntity 方法的工作相对琐碎,首先它会检查当前实体对象的 id 是否发生了变动,如果 id 改

变,即判定为异常(即当前实体对象与 EntityEntry 对应关系非法)。

随即调用 Interceptor(如果有的话)并执行相关的拦截方法。

之后再结合 TypeFactory,Type 等辅助类,将当前实体对象的属性与 EntityEntry 中的原始实体状

态进行比对,判断是否发生了变化,如果发生了变化,是否需要执行数据库更新。

如果以上条件都满足的话,则向当前的更新任务队列中添加一个新的更新任务

(ScheduledUpdate)。

此更新任务队列将在 Session.flush 方法中稍后的 execute 过程被翻译成对应的 update sql 交由数据

库执行。

之后,Transaction 调用当前 Session 所对应的 JDBC Connection 的 commit 方法,将当前事务提交。

至此,user 对象的更新过程完成。

其间的过程,以 UML 序列图表示大致如图 5-3 所示。

unsaved-value

数据保存时,Hibernate 将根据这个值来判断对象是否需要保存。

所谓显式保存,是指代码中明确调用 session 的 save、update、saveOrupdate 方法对对象进行持久

化。如:

session.save(user);

Page 15: Hibernate的高级操作

图 5-3 User 对象更新过程的 UML 序列图

而在某些情况下,如映射关系中,Hibernate 根据级联(Cascade)关系对联接类进行保存。此时

代码中没有针对级联对象的显示保存语句,需要 Hibernate 根据对象当前状态判断是否需要保存到数

据库。此时,Hibernate 即将根据 unsaved-value 进行判定。

首先 Hibernate 会取出目标对象的 id。

之后,将此值与 unsaved-value 进行比对,如果相等,则认为目标对象尚未保存,否则,认为对

象已经保存,无需再进行保存操作。

如:user 对象是之前由 Hibernate 从数据库中获取,同时,此 user 对象的若干个关联对象 address

也被加载,此时我们向 user 对象新增一个 address 对象,此时调用 session.save(user),Hibernate 会根

据 unsaved-value 判断 user 对象的数个 address 关联对象中,哪些需要执行 save 操作,而哪些不需要。

对于我们新加入的 address 对象而言,由于其 id(Integer 型)尚未赋值,因此为 null,与我们设

定的 unsaved-value(null)相同,因此 Hibernate 视其为一个未保存对象,将为其生成 insert 语句并执

行。

这里可能会产生一个疑问,如果“原有”关联对象发生变动(如 user 的某个“原有”的 address

对象的属性发生了变化,所谓“原有”即此 address 对象已经与 user 相关联,而不是我们在此过程中

为之新增的),此时 id 值是从数据库中读出的,并没有发生改变,自然与 unsaved-value(null)也不

一样,那么 Hibernate 是不是就不进行保存操作?

上面关于 PO、VO 的讨论中曾经涉及到数据保存的问题,实际上,这里的“保存”,实际上是“insert”

的概念,只是针对新关联对象的加入,而非数据库中原有关联对象的“update”。所谓新关联对象,一

般情况下可以理解为未与 Session 发生关联的 VO。而“原有”关联对象,则是 PO。如上面关于 PO、

Page 16: Hibernate的高级操作

VO 的讨论中所述:

对于save操作而言,如果对象已经与Session相关联(即已经被加入Session的实体容器

中),则无需进行具体的操作。因为之后的Session.flush过程中,Hibernate会对此实体容器

中的对象进行遍历,查找出发生变化的实体,生成并执行相应的update语句。

5.1.3 数据缓存

数据缓存概述

在特定硬件基础上(假设系统不存在设计上的缺漏和糟糕低效的 SQL 语句)缓存(Cache)往往

是提升系统性能的关键因素。

而对于 ORM 实现而言,缓存则显得尤其重要,它是持久层性能提升的关键。相对 JDBC 数据存

取,ORM 实现往往需要借助更加复杂的机制,以实现内部状态的管理、OR 关系的映射等。

这些额外的开销使得 ORM 数据访问效率相对降低。如何弥补这里产生的性能差距?数据缓存是

其中一个关键策略。

缓存是数据库数据在内存中的临时容器,它包含了库表数据在内存中的临时拷贝,位于数据库与

数据访问层之间(图 5-4)。

Data Access Layer

Data Cache

Database

图 5-4 缓存

ORM 在进行数据读取时,会根据其缓存管理策略,首先在缓存中查询,如果在缓存中发现所需

数据(缓存命中),则直接以此数据作为查询结果加以利用,从而避免了数据库调用的性能开销。

相对内存操作而言,数据库调用是一个代价高昂的过程,对于典型企业级应用结构,数据库往往

与应用服务器位于不同的物理服务器,这也就意味着每次数据库访问都是一次远程调用,Socket 的创

建与销毁,数据的打包拆包,数据库执行查询指令、网络传输上的延时,这些消耗都给系统整体性能

造成了严重影响。

此时,本地内存中数据缓存的存在价值就显得特别突出。特别是对于查询操作相对频繁

(read-mostly)的系统而言(如论坛系统,新闻发布系统等),良好的缓存管理机制以及合理的缓存

应用模式往往是性能提升的关键。

下面,我们就将围绕数据缓存的一般实施策略,及其应用模式进行探讨。

数据缓存策略

持久层设计中,往往需要考虑到几个不同层次中的数据缓存策略。这些层次的划分标准针对不同

Page 17: Hibernate的高级操作

的情况有所差异,一般而言,ORM 的数据缓存应包含如下几个层次:

1. 事务级缓存(Transaction Layer Cache)

2. 应用级/进程级缓存(Application/Process Layer Cache)

3. 分布式缓存(Cluster Layer Cache)

事务级缓存

在当前事务范围内的数据缓存策略。

这里的事务可能是一个数据库事务,也可能是某个应用级事务。对于 Hibernate 而言,事务级缓

存是基于 Session 生命周期实现的,每个 Session 会在内部维持一个数据缓存,此缓存随着 Session 的

创建(销毁)而存在(消亡),因此也称为 Session Level Cache(也称为内部缓存)。

应用级缓存

在某个应用中,或者应用中某个独立数据访问子集中的共享缓存。

此缓存可由多个事务(数据库事务或者应用级事务)共享。事务之间的缓存共享策略与应用的事

务隔离机制密切相关。在 Hibernate 中,应用级缓存在 SessionFactory 层实现,所有由此 SessionFactory

创建的 Session 实例共享此缓存,因此也称为 SessionFactory Level Cache。

多实例并发运行的环境(如多机负载均衡环境中)中,我们必须特别小心缓存机制可能带来的负

面效应。

假设实例 A 和实例 B 共享同一数据库,并行运行,A 和 B 各自维持自己的缓存,如果缺乏同步

机制,A 在某个操作中对数据库进行了更新,而 B 并没有获得相应的更新通知,其缓存中的数据还是

数据库修改之前的版本,那么 B 在之后的读取操作中,可能就以此过期数据作为数据源,从而导致数

据同步错误,这样的错误对于关键业务数据而言是无法承受的(如账务系统)。

在这种情况下,应用级缓存无法使用。为了解决这个问题,我们引入了分布式缓存。

分布式缓存

在多个应用实例,多个 JVM 之间共享的缓存模式。

分布式缓存由多个应用级缓存实例组成集群,通过某种远程机制(如 RMI 或 JMS)实现各个缓

存实例间的数据同步,任何一个实例的数据修改操作,将导致整个集群间的数据状态同步。

分布式缓存解决了多实例并发运行过程中的数据同步问题。

但是,除非对于并发读取性能要求较高,且读取操作在持久层操作中占绝大部分比重的情况,分

布式缓存的实际效果尚需考证。

由于多个实例间的数据同步机制,每个缓存实例发生的变动都会复制到其余所有节点中(对于

Repplication 式缓存而言),这样的远程同步开销不可忽视。

笔者曾经主持构建的一个大型金融业务系统中,在模拟测试阶段发现,分布式缓存的频繁同步甚

至导致了网络中的数据阻塞。

考虑到主流企业级数据库均已经具备了数据库级的缓存机制,此时,分布式缓存的性能优势在一

些情况下并不明显,并且还可能引入其他的问题,因此,分布式缓存的使用还有待商榷。当我们决定

在系统中引入分布式缓存前,必须经过仔细的压力测试和性能分析,以免出现不必要的尴尬。

另外,需要再次强调的是,如果当前应用与其他应用共享数据库,也就是说,在当前应用运行过

程中,其他应用可能同时更新数据库,那么缓存策略的制定就需要格外小心。这种情况下,采取一些

Page 18: Hibernate的高级操作

保守策略(避免缓存机制的使用)可能更加稳妥。

5.1.4 Hibernate 数据缓存

Hibernate 数据缓存(Cache)分为两个层次,以 Hibernate 语义加以区分,可分为:

1. 内部缓存(Session Level,也称为一级缓存)

2. 二级缓存(SessionFactory Level,也称为二级缓存)

Hibernate 中,缓存将在以下情况中发挥作用:

1. 通过 id [主键] 加载数据时

这包括了根据 id查询数据的 Session.load方法,以及 Session.iterate等批量查询方法(Session.iterate

进行查询时,也是根据 id 在缓存中查找数据,类似一个 Session.load 循环,具体请参见“持久化

操作”部分的讨论)。

2. 延迟加载

缓存的应用是一个非常复杂的论题,在下面的内容中,我们将主要围绕 Hibernate 中数据缓存的

概念及其运行机制进行讨论。而缓存的具体使用,以及结合缓存的数据访问策略和技巧,将在“持

久化操作”部分结合对应的数据访问方法进行探讨。

内部缓存

内部缓存在 Hibernate 中又称为一级缓存,属于应用事务级缓存。

在之前的“脏数据检查”部分的讨论中,实际上我们已经涉及了内部缓存的实现原理。

Session 在内部维护了一个 Map 数据类型,此数据类型中保持了所有的与当前 Session 相关联的数

据对象,如果观察 SessionImpl 类源码,我们可以看到: private final Map entitiesByKey; //key=Key, value=Object private final Map proxiesByKey; //key=Key, value=HibernateProxy private transient Map entityEntries; //key=Object, value=Entry private transient Map arrayHolders; //key=array, value=ArrayHolder private transient Map collectionEntries; //key=PersistentCollection, value=CollectionEntry private final Map collectionsByKey; //key=CollectionKey, value=PersistentCollection

这些 Map 数据结构中维护了当前 Session 中所有相关 PO 的状态。

如果我们需要通过 Session 加载某个数据对象,Session 首先会根据所要加载的数据类和 id,在

entitiesByKey 中寻找是否已有此数据的缓存实例,如果存在且其状态判定为有效,则以此数据实例作

为结果返回。

同样,如果 Session 从数据库中加载了数据,也会将其纳入此 Map 结构加以管理。

这也就是内部缓存的实现,非常简单。另外,根据代码可以看出,这些 Map 数据结构为 Session

的私有数据,伴随 Session 实例的创建而创建,消亡而消亡。因此,有时也称此缓存为 Session Level

Cache。

内部缓存正常情况下由 Hibernate 自动维护,如果需要手动干预,我们可以通过以下方法完成:

1. Session.evict

Page 19: Hibernate的高级操作

将某个特定对象从内部缓存中清除。

2. Session.clear

清空内部缓存。

二级缓存

在 Hibernate 中,二级缓存涵盖了应用级缓存和分布式缓存领域。

二级缓存将由从属于本SessionFactory的所有Session实例共享,因此有时称为SessionFactory Leve

Cache。

Session 在进行数据查询操作时,会首先在自身内部的一级缓存中进行查找,如果一级缓存未能

命中,则将在二级缓存中查询,如果二级缓存命中,则以此数据作为结果返回。

在引入二级缓存时,我们首先必须考虑以下问题:

1. 数据库是否与其他应用共享

2. 应用是否需要部署在集群环境中

对于第一种情况,往往也就意味着我们不得不放弃二级缓存的使用(我们也可以对数据库的共享

情况进行细化,比如某个表由本应用独占,那么也可以对此表引用二级缓存机制)。

对于第二种情况,我们就必须考虑是否需要引入分布式缓存机制,以及引入分布式缓存带来的实

际性能变化。

其次,我们应该对哪些数据应用二级缓存?

显然,对数据库中所有的数据都实施缓存是 简单的方法,大多数情况下,这可能也是实际开发

中 常采用的模式(节省了开发人员的大量脑细胞☺)。

但是在某些情况下,这样的方式反而会对性能造成影响,如对于以下情况:一个电信话务系统,

客户可以通过这套系统查询自己的历史通话记录。

这个案例中,对于每个客户,库表中可能都有成千上万条数据,而不同客户之间,基本不可能共

享数据(客户只能查询自身的通话记录),如果对此表施以缓存管理,那么可以想象,内存会迅速被

几乎不可能再被重用的数据充斥,系统性能急剧下降。

因此,在考虑缓存机制应用策略的时候,我们必须对当前系统的数据逻辑进行考察,以确定 佳

的解决方案。

如果数据满足以下条件,则可将其纳入缓存管理。

1. 数据不会被第三方应用修改

2. 数据大小(Data Size)在可接受的范围之内

3. 数据更新频率较低

4. 同一数据可能会被系统频繁引用

5. 非关键数据(关键数据,如金融账户数据)

Hibernate 本身并未提供二级缓存的产品化实现(只是提供了一个基于 Hashtable 的简单缓存以供

调试),而是为众多的第三方缓存组件提供了接入接口,我们可以根据实际情况选择不同的缓存实现

版本,具体请参见稍后的“第三方缓存实现”部分内容的描述。

第三方缓存实现

Page 20: Hibernate的高级操作

基于 Java 的缓存实现, 简单的方式莫过于对集合类数据类型进行封装。Hibernate 提供了基于

Hashtable 的缓存实现机制,不过,由于其性能和功能上的局限,仅供开发调试中使用。

同时,Hibernate 还提供了面向第三方缓存实现的接口,如:

1. JCS

2. EHCache

3. OSCache

4. JBoss Cache

5. SwarmCache

Hibernate 早期版本中采用了 JCS(Java Caching System ——Apache Turbine 项目中的一个子项目)

作为默认的二级缓存实现。由于 JCS 的发展停顿2,以及其内在的一些问题(在某些情况下,可能导

致内存泄漏以及死锁),新版 Hibernate 已经将 JCS 去除,并以 EHCache 作为其默认的二级 Cache 实

现。

相对 JCS 而言,EHCache 更加稳定,并具备更好的缓存调度性能,其缺陷是目前还无法做到分布

式缓存。

如果我们的系统需要在多台设备上部署,并共享同一个数据库(典型的,如多机负载均衡),则

必须使用支持分布式缓存的 Cache 实现(如 JBossCache)以避免出现不同系统实例之间缓存不一致,

而导致数据同步错误的情况。

Hibernate 对缓存进行了良好封装,透明化的缓存机制使得我们在上层结构的实现中无需面对繁琐

的缓存维护细节。

目前 Hibernate 支持的缓存实现在表 5-1 中列出,注意 Hibernate3 中 provider_class 包名需要修改

为 org.hibernate.cache。

表 5-1 Hibernate 支持的缓存实现

名 称 provider_class 分布式支持 查询缓冲

HashTable net.sf.hibernate.cache.HashtableCacheProvider N Y

EHCache net.sf.ehcache.hibernate.Provider N Y

OSCache net.sf.hibernate.cache.OSCacheProvider N Y

SwarmCache net.sf.hibernate.cache.SwarmCacheProvider Y N

JBossCache net.sf.hibernate.cache.TreeCacheProvider Y Y

SwarmCache 和 JBossCache 均提供了分布式缓存实现(Cache 集群)。

其中 SwarmCache 提供的是 invalidation 方式的分布式缓存,即当集群中的某个节点更新了缓存中

的数据,即通知集群中的其他节点将此数据废除,之后各个节点需要用到这个数据的时候,会重新从

数据库中读入并填充到缓存中。

而 JBossCache 提供的是 Repplication 式的缓存,即如果集群中某个节点的数据发生改变,此节点

会将发生改变的数据的 新版本复制到集群中的每个节点中以保持所有节点状态一致。

Hibernate 中启用二级缓存,需要在 hibernate.cfg.xml 中配置以下参数(以 EHCache 为例): <hibernate-configuration> <session-factory>

2 本书截稿之前,Apache组织将JCS项目提升到主项目层次,相信不久之后会有一个更加成熟可靠的版本出现。

Page 21: Hibernate的高级操作

…… <property name="hibernate.cache.provider_class"> net.sf.ehcache.hibernate.Provider </property> …… </session-factory> </hibernate-configuration>

另外还需要针对 Cache 实现本身进行配置,下面是一个 EHCache 配置文件示例:ehcache.xml: <ehcache> <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="10000" //Cache中最大允许保存的数据对象数量 eternal="false" //Cache中数据是否为常量 timeToIdleSeconds="120" //缓存数据钝化时间 timeToLiveSeconds="120" //缓存数据的生存时间 overflowToDisk="true" //内存不足时,是否启用磁盘缓存 /> </ehcache>

(其中“//”开始的注释是笔者追加,实际配置文件中不应出现)

之后,需要在我们的映射文件中指定各个映射实体(以及 collection)的缓存同步策略: <class name=" org.hibernate.sample.TUser" .... > <cache usage="read-write"/> .... <set name="addresses" .... > <cache usage="read-only"/> .... </set> </class>

缓存同步策略可应用于实体类和集合属性。

下面,我们继续围绕缓存同步策略进行探讨。

缓存同步策略

缓存同步策略决定了数据对象在缓存中的存取规则。

为了使得缓存调度遵循正确的应用级事务隔离机制,我们必须为每个实体类指定相应的缓存同步

策略。

Hibernate 提供以下 4 种内置的缓存同步策略:

read-only

只读。对于不会发生改变的数据,可使用只读型缓存。

nonstrict-read-write

如果程序对并发访问下的数据同步要求不是非常严格,且数据更新操作频率较低(几个

小时或者更长时间更新一次),可以采用本选项,获得较好的性能。

read-write

严格可读写缓存。基于时间戳判定机制,实现了“read committed”事务隔离等级3。可

用于对数据同步要求严格的情况,但不支持分布式缓存。这也是实际应用中使用 多的同步

策略。

3 参见稍后“事务管理”部分中对于事务隔离等级的描述。

Page 22: Hibernate的高级操作

transactional

事务型缓存,必须运行在 JTA 事务环境中。

在事务型缓存中,缓存的相关操作也被添加到事务之中(此时的缓存,类似一个内存数据库),

如果由于某种原因导致事务失败,我们可以连同缓冲池中的数据一同回滚到事务开始之前的状态。

事务型缓存实现了“Repeatable read”事务隔离等级,有效保障了数据的合法性,适用于对关键

数据的缓存。

注意:目前 Hibernate 内置的 Cache 中,只有 JBossCache 支持事务性的 Cache 实现。

不同的缓存实现,可支持的缓存同步策略也各不相同(表 5-2)。

表 5-2 不同缓存实现所对应的同步策略

名 称 read-only read-write nonstrict-read-write transactional

HashTable Y Y Y

EHCache Y Y Y

OSCache Y Y Y

SwarmCache Y Y

JBossCache Y Y

5.1.5 事务管理

事务管理概述

“事务”是一个逻辑工作单元,它包括一系列的操作。事务包含 4 个基本特性,也就是我们常说

的 ACID,其中包括:

1. Atomic(原子性,这里的“原子”即代表事务中的各个操作不可分割)

事务中包含的操作被看作一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失

败。

如:A 通过某网上银行系统给 B 转账,此时会执行两个数据更新操作,减少 A 的余额,增

加 B 的余额。这两个操作形成一个事务,在此事务内的这两个更新操作必须符合“要么全部成

功,要么全部失败”的事务原子性原则。单纯 A 余额的减少或者 B 余额的增加都会造成账务系

统的混乱。

2. Consistency(一致性)

一致性意味着,只有合法的数据可以被写入数据库,如果数据有任何违例(比如数据与字段

类型不符),则事务应该将其回滚到 初状态。

3. Isolation(隔离性)

事务允许多个用户对同一个数据的并发访问,而不破坏数据的正确性和完整性。同时,并行

事务的修改必须与其他并行事务的修改相互独立。

按照比较严格的隔离逻辑来讲,一个事务看到的数据要么是另外一个事务修改这些事务之前

的状态,要么是第二个事务已经修改完成的数据,但是这个事务不能看到其他事务正在修改的数

据。

针对不同的情况,事务的隔离级别要求也各有差异,下面一节中,我们将具体探讨事务隔离

Page 23: Hibernate的高级操作

等级的相关内容。

4. Durability(持久性)

事务结束后,事务处理的结果必须能够得到固化(保存在可掉电存储器上)。

数据库事务管理隔离等级

事务隔离指的是,数据库(或其他事务系统)通过某种机制,在并行的多个事务之间进行分隔,

使每个事务在其执行过程中保持独立(如同当前只有此事务单独运行)。

本节内容主要围绕数据库事务隔离等级进行探讨。

Hibernate 中的事务隔离依赖于底层数据库提供的事务隔离机制,因此,对数据库事务隔离机制的

理解在基于 Hibernate 实现的持久层中同样适用。

首先我们来看数据操作过程中可能出现的 3 种不确定情况:

脏读取(Dirty Reads)

一个事务读取了另一个并行事务未提交的数据。

不可重复读取(Non-repeatable Reads)

一个事务再次读取之前曾读取过的数据时,发现该数据已经被另一个已提交的事务修

改。

虚读(Phantom Reads)

一个事务重新执行一个查询,返回一套符合查询条件的记录,但这些记录中包含了因

为其他 近提交的事务而产生的新记录。

为了避免上面 3 种情况的出现。标准 SQL 规范中,定义了如下 4 个事务隔离等级:

Read Uncommitted

低等级的事务隔离,它仅仅保证了读取过程中不会读取到非法数据。这种隔离等级

下,上述 3 种不确定情况均有可能发生。

此事务等级对于大多数逻辑严格的应用系统而言是难以接受的,脏读取的出现将为系

统的并发逻辑带来极大的隐患。

Read Committed

此级别的事务隔离保证了一个事务不会读到另一个并行事务已修改但未提交的数据,

也就是说,此等级的事务级别避免了“脏读取”。

当一个事务运行在这个隔离级别时, 一个 SELECT 查询只能看到查询开始之前提交

的数据,而永远无法看到未提交的数据,或者是在查询执行时其他并行的事务提交做的改

变。

此事务隔离等级是大多数主流数据库的默认事务等级,同时也适用于大多数系统。

Repeatable Read

此级别的事务隔离避免了“脏读取”和“不可重复读取”现象的出现。这也意味着,一

个事务不可能更新已经由另一个事务读取但未提交(回滚)的数据。

一般而言,此级事务应用并不广泛,它并不能完全保证数据的合法性(可能出现虚读),

同时也带来了更多的性能损失,如果当前数据库由应用所独享,那么我们可以考虑通过“乐

观锁”达到同样的目的(参见稍后关于“锁”机制的探讨)。

Page 24: Hibernate的高级操作

Serializable

高等级的事务隔离,也提供了 严格的隔离机制。 上面 3 种不确定情况都将被规避。

这个级别将模拟事务的串行执行,逻辑上如同所有事务都处于一个执行队列,依次串行执

行,而非并行执行。

此事务隔离等级在提供了 严密的隔离机制的同时,无疑也带来了高昂的性能开销。

因此使用必须谨慎。生产系统中很少有使用此级事务隔离等级的案例。如果确实需要,我

们可以通过一些其他的策略加以实现(如“悲观锁”机制,参见稍后关于“锁”机制的探

讨)。

这 4 种事务隔离等级总结如下(表 5-3):

表 5-3 事务隔离等级

隔离等级 脏读取 不可重复读取 虚读

Read Uncommitted 可能 可能 可能

Read Committed 不可能 可能 可能

Repeatable Read 不可能 不可能 可能

Serializable 不可能 不可能 不可能

这 4 种事务隔离等级的严密程度由前往后依次递增,同时,其性能也依次下降。因此,无论实际

情况如何,都使用 高级事务隔离的做法并不可取。我们必须根据应用的具体情况进行取舍,以获得

数据合法性与系统性能上的 佳平衡。

Hibernate 事务管理概述

Hibernate 是 JDBC 的轻量级封装,本身并不具备事务管理能力。在事务管理层,Hibernate 将其

委托给底层的 JDBC 或者 JTA,以实现事务的管理和调度。

Hibernate 的默认事务处理机制基于 JDBC Transaction。我们也可以通过配置文件设定采用 JTA 作

为事务管理实现: <hibernate-configuration> <session-factory> …… <property name="hibernate.transaction.factory_class"> net.sf.hibernate.transaction.JTATransactionFactory <!--net.sf.hibernate.transaction.JDBCTransactionFactory--> </property> …… </session-factory> </hibernate-configuration>

基于 JDBC 的事务管理

将事务管理委托给 JDBC 进行处理无疑是 简单的实现方式,Hibernate 对于 JDBC 事务的封装也

非常简单。

我们来看下面这段代码:

session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); …… tx.commit();

从 JDBC 层面而言,上面的代码实际上对应着:

Page 25: Hibernate的高级操作

Connection dbconn = getConnection(); dbconn.setAutoCommit(false); …… dbconn.commit();

就是这么简单,Hibernate 并没有做更多的事情(实际上也没法做更多的事情),只是将这样的

JDBC 代码进行了封装而已。

这里要注意的是,在 sessionFactory.openSession()中,Hibernate 会初始化数据库连接,与此同时,

将其 AutoCommit 设为关闭状态(false)。而其后,在 Session.beginTransaction 方法中,Hibernate 会

再次确认 Connection 的 AutoCommit 属性被设为关闭状态(为了防止用户代码对 session 的

Connection.AutoCommit 属性进行修改)。

这也就是说,我们一开始从 SessionFactory 获得的 session,其自动提交属性就已经被关闭

(AutoCommit=false),下面的代码将不会对事务性数据库产生任何效果(非事务性数据库除外,如

Mysql ISAM):

session = sessionFactory.openSession(); session.save(user); session.close();

这实际上相当于 JDBC Connection 的 AutoCommit 属性被设为 false,执行了若干 JDBC 操作之后,

没有调用 commit 操作即将 Connection 关闭。

如果要使代码真正作用到数据库,我们必须显式地调用 Transaction 指令: session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); session.save(user); tx.commit(); session.close();

基于 JTA 的事务管理

JTA 提供了跨 Session 的事务管理能力。这一点是与 JDBC Transaction 大的差异。

JDBC 事务由 Connection 管理,也就是说,事务管理实际上是在 JDBC Connection 中实现。事务

周期限于 Connection 的生命周期之类。同样,对于基于 JDBC Transaction 的 Hibernate 事务管理机制

而言,事务管理在 Session 所依托的 JDBC Connection 中实现,事务周期限于 Session 的生命周期。

JTA 事务管理则由 JTA 容器实现,JTA 容器对当前加入事务的众多 Connection 进行调度,实现

其事务性要求。JTA 的事务周期可横跨多个 JDBC Connection 生命周期。同样对于基于 JTA 事务的

Hibernate 而言,JTA 事务横跨可横跨多个 Session。

图 5-5 形象地说明了这个问题:

JDBC Connection

TransactionStart

TransactionCommit

JTA Container

JDBC Connection 1 JDBC Connection 1 JDBC Connection 1……

Transaction Commit

TransactionStart

JDBC Transaction

JTA Transaction

Page 26: Hibernate的高级操作

图 5-5 基于 JDBC 的事务管理与基于 JTA 的事务管理

图中描述的是 JDBC Connection 与事务之间的关系,而 Hibernate Session 在这里与 JDBC

Connection 具备同等的逻辑含义。

从图 5-5 中我们可以看出,JTA 事务是由 JTA Container 维护的,事务的生命周期由 JTA Container

维护,而与具体的 Connection 无关。

这里需要注意的是,参与 JTA 事务的 Connection 需避免对事务管理进行干涉。这也就是说,如

果采用 JTA Transaction,我们不应该再调用 Hibernate 的 Transaction 功能。

上面基于 JDBC Transaction 的正确代码,这里就会产生问题: public class ClassA{ public void saveUser(User user){ Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); session.save(user); tx.commit(); session.close(); } } public class ClassB{ public void saveOrder(Order order){ Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); session.save(order); tx.commit(); session.close(); } } public class ClassC{ public void save(){ …… UserTransaction tx = (UserTransaction)( new InitialContext().lookup(“……”) ); ClassA.save(user); ClassB.save(order); tx.commit(); …… } }

这里有两个类 ClassA 和 ClassB,分别提供了两个方法:saveUser 和 saveOrder,用于保存用户信

息和订单信息。在 ClassC 中,我们顺序调用了 ClassA.saveUser 方法和 ClassB.saveOrder 方法,同时

引入了 JTA 中的 UserTransaction 以实现 ClassC.save 方法中的事务性。

问题出现了,ClassA 和 ClassB 中分别都调用了 Hibernate 的 Transaction 功能。在 Hibernate 的 JTA

封装中,Session.beginTransaction 同样也执行了 InitialContext.lookup 方法获取 UserTransaction 实例,

Transaction.commit 方法同样也调用了 UserTransaction.commit 方法。实际上,这就形成了两个嵌套式

的 JTA Transaction:ClassC 声明了一个事务,而在 ClassC 事务周期内,ClassA 和 ClassB 也企图声明

自己的事务,这将导致运行期错误。

因此,如果决定采用 JTA Transaction,应避免再重复调用Hibernate的Transaction功能,上面ClassA

和 ClassB 的代码修改如下: public class ClassA{

Page 27: Hibernate的高级操作

public void save(TUser user){ Session session = sessionFactory.openSession(); session.save(user); session.close(); } …… } public class ClassB{ public void save (Order order){ Session session = sessionFactory.openSession(); session.save(order); session.close(); } …… }

上面代码中的 ClassC.save 方法,同时修改如下: public class ClassC{ public void save(){ …… Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); classA.save(user); classB.save(order); tx.commit(); …… } }

实际上,这是利用 Hibernate 来完成启动和提交 UserTransaction 的功能,但这样的做法比原本直

接通过 InitialContext 获取 UserTransaction 的做法消耗了更多的资源,得不偿失。

在 EJB 中使用 JTA Transaction 无疑 为简便,我们只需要将 save 方法配置为 JTA 事务支持即可,

无需显式声明任何事务,下面是一个 Session Bean 的 save 方法,它的事务属性被声明为“Required”,

EJB 容器将自动维护此方法执行过程中的事务: /** * @ejb.interface-method * view-type="remote" * * @ejb.transaction type = "Required" **/ public void save(){ //EJB环境中,通过部署配置即可实现事务声明,而无需显式调用事务 classA.save(user); classB.save(log); }//方法结束时,如果没有异常发生,则事务由EJB容器自动提交。

锁(locking)

业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算处理中,我

们希望针对某个截止点的数据进行处理,而不希望在结算进行过程中(可能是几秒钟,也可能是几个

小时),数据再发生变化。

此时,我们就需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,这样的机制,

在这里,也就是所谓的“锁”,即给我们选定的目标数据上锁,使其无法被其他程序修改。

Hibernate 支持两种锁机制:即通常所说的“悲观锁(Pessimistic Locking)”和“乐观锁(Optimistic

Locking)”。

Page 28: Hibernate的高级操作

悲观锁(Pessimistic Locking)

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的

事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,

往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,

即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

一个典型的,依赖数据库实现的悲观锁调用:

select * from account where name="Erica " for update

通过 for update 子句,这条 SQL 锁定了 account 表中所有符合检索条件(name=“Erica”)的记录。

本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。

Hibernate 的悲观锁,也是基于数据库的锁机制实现。

下面的代码实现了对查询记录的加锁:

String hqlStr = "from TUser as user where user.name='Erica'"; Query query = session.createQuery(hqlStr); query.setLockMode("user",LockMode.UPGRADE); //加锁 List userList = query.list();//执行查询,获取数据

query.setLockMode 对查询语句中,特定别名所对应的记录进行加锁(我们通过“from TUser as

user”为 TUser 类指定了一个别名“user”),这里也就是对返回的所有 user 记录进行加锁。

观察运行期 Hibernate 生成的 SQL 语句: select tuser0_.id as id, tuser0_.name as name, tuser0_.group_id as group_id,

tuser0_.user_type as user_type, tuser0_.sex as sex from t_user tuser0_ where (tuser0_.name='Erica' ) for update

可以看到 Hibernate 通过使用数据库的 for update 子句实现了悲观锁机制。

Hibernate 的加锁模式有:

LockMode.NONE : 无锁机制

LockMode.WRITE :Hibernate 在 Insert 和 Update 记录的时候会自动获取

LockMode.READ : Hibernate 在读取记录的时候会自动获取

以上这 3 种锁机制一般由 Hibernate 内部使用,如 Hibernate 为了保证 Update 过程中对象不会被

外界修改,会在 save 方法实现中自动为目标对象加上 WRITE 锁。这些都是 Hibernate 内部对数据的

锁定机制,与数据库无关。

LockMode.UPGRADE :利用数据库的 for update 子句加锁

LockMode. UPGRADE_NOWAIT :Oracle 的特定实现,利用 Oracle 的 for update nowait

子句实现加锁

上面这两种锁机制是我们在应用层较为常用的,依赖数据库的悲观锁机制。

加锁一般通过以下方法实现: Criteria.setLockMode Query.setLockMode Session.lock

注意,只有在查询开始之前(也就是 Hibernate 生成 SQL 之前)设定加锁,才会真正通过数据库

Page 29: Hibernate的高级操作

的锁机制进行加锁处理,否则,数据已经通过不包含 for update 子句的 Select SQL 加载进来,所谓数

据库加锁也就无从谈起。

乐观锁(Optimistic Locking)

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁

机制实现,以保证操作 大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事

务而言,这样的开销往往无法承受。

如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如

更改用户账户余额),如果在其全程都采用悲观锁机制,也就意味着整个操作过程中(从操作员读出

数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录

始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。

乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实

现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为

数据库表增加一个“version”字段来实现。

读取出数据时,将此版本号一同读出,之后更新时,对此版本号加 1。此时,将提交数据的版本

数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本

号,则予以更新,否则认为是过期数据。

对于上面修改用户账户信息的例子而言,假设数据库中账户信息表中有一个 version 字段,当前

值为 1;而当前账户余额字段(balance)为$100。

1. 操作员 A 此时将其读出(version=1),并从其账户余额中扣除$50($100-$50)。

2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息(version=1),并从其账户余额中扣

除$20($100-$20)。

3. 操作员 A 完成了修改工作,将数据版本号加 1(version=2),连同账户扣除后余额

(balance=$50),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被

更新,数据库记录 version 更新为 2。

4. 操作员 B 完成了操作,也将版本号加 1(version=2)试图向数据库提交数据(balance=$80),

但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2,数据库记录当前版本也为

2,不满足“提交版本必须大于记录当前版本才能执行更新”的乐观锁策略,因此,操作员 B 的

提交被驳回。这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操

作结果的可能。

从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A 和操作员 B

操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。

需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上

例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控

制,因此可能会造成非法数据被更新到数据库中。

在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策

略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对

外公开)。

Hibernate 在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数据库的更新操作,

利用 Hibernate 提供的透明化乐观锁实现,将大大提升我们的生产力。

Page 30: Hibernate的高级操作

Hibernate 中可以通过 class 描述符的 optimistic-lock 属性结合 version 描述符指定。

现在,我们为之前示例中的 TUser 加上乐观锁机制。

1. 首先为 TUser 的 class 描述符添加 optimistic-lock 属性: <hibernate-mapping> <class name="org.hibernate.sample.TUser" table="t_user" dynamic-update="true" dynamic-insert="true" optimistic-lock="version" > …… </class> </hibernate-mapping>

optimistic-lock 属性有如下可选取值:

none 无乐观锁。

version

通过版本机制实现乐观锁。

dirty

通过检查发生变动过的属性实现乐观锁。

all

通过检查所有属性实现乐观锁。

其中通过 version 实现的乐观锁机制是 Hibernate 官方推荐的乐观锁实现,同时也是 Hibernate 中,

目前惟一在实体对象脱离 Session 发生修改的情况下依然有效的锁机制。因此,一般情况下,我们都

选择 version 方式作为 Hibernate 乐观锁实现机制。

2. 添加一个 Version 属性描述符 <hibernate-mapping> <class name="org.hibernate.sample.TUser" table="t_user" dynamic-update="true" dynamic-insert="true" optimistic-lock="version" > <id name="id" column="id" type="java.lang.Integer" > <generator class="native"> </generator> </id> <version column="version" name="version" type="java.lang.Integer" /> …… </class> </hibernate-mapping>

Page 31: Hibernate的高级操作

注意,version 节点必须出现在 ID 节点之后。

这里我们声明了一个 version 属性,用于存放用户的版本信息,保存在 T_User 表的 version 字段

中。

此时如果我们尝试编写一段代码,更新 TUser 表中记录的数据,如: Criteria criteria = session.createCriteria(TUser.class); criteria.add(Expression.eq("name","Erica")); List userList = criteria.list(); TUser user =(TUser)userList.get(0); Transaction tx = session.beginTransaction(); user.setUserType(1); //更新UserType字段 tx.commit();

每次对 TUser 进行更新的时候,我们可以发现,数据库中的 version 都在递增。

而如果我们尝试在 tx.commit 之前,启动另外一个 Session,对名为 Erica 的用户进行操作,以模

拟并发更新时的情形: Session session= getSession(); Criteria criteria = session.createCriteria(TUser.class); criteria.add(Expression.eq("name","Erica")); Session session2 = getSession(); Criteria criteria2 = session2.createCriteria(TUser.class); criteria2.add(Expression.eq("name","Erica")); List userList = criteria.list(); List userList2 = criteria2.list(); TUser user =(TUser)userList.get(0); TUser user2 =(TUser)userList2.get(0); Transaction tx = session.beginTransaction(); Transaction tx2 = session2.beginTransaction(); user2.setUserType(99); tx2.commit(); user.setUserType(1); tx.commit();

执行以上代码,代码将在 tx.commit()处抛出 StaleObjectStateException 异常,并指出版本检查失

败,当前事务正在试图提交一个过期数据。通过捕捉这个异常,我们就可以在乐观锁校验失败时进行

相应处理。

5.1.6 持久层操作

在前面“脏数据检查”部分中,我们已经对 Hibernate 数据更新的实现过程进行了部分探讨。

而 Hibernate 持久层操作的内容,则远不止于此。下面,我们就围绕 Hibernate 中常用持久层操作

实现原理进行探讨。了解持久层操作的实现原理,对我们实现高性能的 Hibernate 持久层将别具意义。

数据加载

Session.get/load

Page 32: Hibernate的高级操作

Session.load/get 方法均可以根据指定的实体类和 id 从数据库读取记录,并返回与之对应的实体对

象。

其区别在于:

1. 如果未能发现符合条件的记录, get 方法返回 null ,而 load 方法会抛出一个

ObjectNotFoundException。

2. Load 方法可返回实体的代理类实例,而 get 方法永远直接返回实体类。关于代理的内容请参

见稍后关于“延迟加载”部分内容。

3. load 方法可以充分利用内部缓存和二级缓存中的现有数据,而 get 方法则仅仅在内部缓存中

进行数据查找,如没有发现对应数据,将越过二级缓存,直接调用 SQL 完成数据读取。

首先来看一个 简单的数据加载过程:

TUser user = (TUser)session.load(TUser.class,new Integer(1));

上面的代码,我们根据实体类型(TUser.class),数据 id(1)加载对应的 user 实体。根据实体类

型和数据 id,Hibernate 即可判定需要读取的库表并定位数据记录。

Session 在加载实体对象时,将经过哪些过程?

1. 首先,通过之前的讨论我们知道,Hibernate 中维持了两级缓存。第一级缓存由 Session 实例

维护,其中保持了 Session 当前所有关联实体的数据,也称为内部缓存。而第二级缓存则存

在于 SessionFactory 层次,由当前所有由本 SessionFactory 构造的 Session 实例共享。

出于性能考虑,避免无谓的数据库访问,Session 在调用数据库查询功能之前,会先在缓存

中进行查询。首先在第一级缓存中,通过实体类型和 id 进行查找,如果第一级缓存查找命

中,且数据状态合法,则直接返回。

2. 之后,Session 会在当前“NonExists”记录中进行查找,如果“NonExists”记录中存在同样

的查询条件,则返回 null。

“NonExists”记录了当前 Session 实例在之前所有查询操作中,未能查询到有效数据的查询

条件(相当于一个查询黑名单列表)。如此一来,如果 Session 中一个无效的查询条件重复出

现,即可迅速做出判断,从而获得 佳的性能表现。

3. 对于 load 方法而言,如果内部缓存中未发现有效数据,则查询第二级缓存,如果第二级缓

存命中,则返回。

4. 如在缓存中未发现有效数据,则发起数据库查询操作(Select SQL),如经过查询未发现对应

记录,则将此次查询的信息在“NonExists”中加以记录,并返回 null。

5. 根据映射配置和 Select SQL 得到的 ResultSet,创建对应的数据对象。

6. 将其数据对象纳入当前 Session 实体管理容器(一级缓存)。

7. 执行 Interceptor.onLoad 方法(如果有对应的 Interceptor)。

8. 将数据对象纳入二级缓存。

9. 如果数据对象实现了 LifeCycle 接口,则调用数据对象的 onLoad 方法。

10. 返回数据对象。

Session.find/iterate

查询性能往往是系统性能表现的一个重要方面。相对数据库更新、删除操作而言,查询机制的优

Page 33: Hibernate的高级操作

劣很大程度上决定了系统的整体性能。

同样,这个领域,往往也存在 大的性能调整空间。对于同样的查询结果,不同的实现机制其性

能差距可能超出大多数人的想象(出现几百倍的性能差距并不奇怪)。

因此,在开始具体应用开发之前,了解这方面的实现原理和机制是非常必要的,而这,也是我们

下面即将讨论的主题。

Hibernate 2 中,Session 接口提供了以下方法以完成数据的批量查询功能(相对于 Session.load 的

单一数据加载而言): public List find(…); public Iterator iterate(…);

Hibernate 查询接口 Query、Criteria 的查询功能,其内部也正是基于这两个方法实现,因此,对

Session.find/iterate 方法的讨论,涵盖了 Hibernate 中数据批量查询的主要领域,值得引起特别的关注。

另外,值得注意的是:Hibernate3 中,上述方法已经从 Session 接口中废除,统一由 Query 接口

提供。find、iterate 分别对应于 Query.list 和 Query.iterate 方法,对应关系如表 5-4 所示。

表 5-4 Hibernate 2 与 Hibernaet 3 中方法的对应关系

Hibernate 2 Hibernate 3

Session.find() session.createQuery().list()

Session.iterate() session.createQuery().iterate()

从实现机制而言,这两个版本之间并没有什么差异。

在下面的内容中,为了保持语义一致性,我们以 Hibernate 2 作为基准版本进行描述。

find/iterate方法均可根据指定条件查询并返回符合查询条件的实体对象集。如: //Session.find String hql = "from TUser where age > ?"; List userList = session.find(hql,new Integer(18),Hibernate.INTEGER); int len = userList.size(); for (int i=0;i<len;i++){ TUser user = (TUser)userList.get(i); System.out.println("User Name:"+user.getName()); }

运行上面的代码,得到屏幕输出:

Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age, tuser0_.version as version from t_user tuser0_ where (age>? ) User Name:Emma User Name:Sammi

运行下面的代码:

//Session.iterate String hql = "from TUser where age > ?"; Iterator it = session.iterate( hql, new Integer(18), Hibernate.INTEGER ); while (it.hasNext()){ TUser user = (TUser)it.next(); System.out.println("User Name:"+user.getName()); }

Page 34: Hibernate的高级操作

得到屏幕输出:

Hibernate: select tuser0_.id as x0_0_ from t_user tuser0_ where (age>? ) Hibernate: select tuser0_.id as id0_, tuser0_.name as name0_, tuser0_.age as age0_, tuser0_.version as version0_ from t_user tuser0_ where tuser0_.id=? Hibernate: select tuser0_.id as id0_, tuser0_.name as name0_, tuser0_.age as age0_, tuser0_.version as version0_ from t_user tuser0_ where tuser0_.id=? User Name:Emma User Name:Sammi

可以看到,Session.find/iterate方法实现了相同的功能——根据查询条件从数据库获取符合条件

的记录,并返回对应的实体集。

从表象上来看,这两个方法达到了同样的目的,只是返回的集合类型不同,find方法返回List,iterate

返回Iterator。这两个方法的区别是否仅限于集合操作的方式差异?

对比上面的输出日志,相信大家都会产生一些疑惑,这两个方法调用的SQL并不一致,那么是否

其实现机制上也有所不同?

显然,find方法通过一条Select SQL实现了查询操作,而iterate方法,则执行了3次Select SQL,第

一次获取了所有符合条件的记录的id,之后,再根据各个id从库表中读取对应的记录,这是一个典型

的N+1次查询问题。

对于这里的例子,库表中有两条符合查询提交的记录,就需要执行2+1=3条Select语句。iterate方

法导致的N+1次查询相对list方法的一次查询,无疑性能较为低下。如果符合条件数据有100 000万条,

那么就要执行100000+1条Select SQL,可想是怎样的性能噩梦。

既然如此,为何Hibernate还要提供iterator方法,而不是仅仅提供高效的find方法?

这个问题与Hibernate缓存机制密切相关。

尝试运行以下代码: String hql = "from TUser where age > ?"; List userList = session.find(hql,new Integer(18),Hibernate.INTEGER); int len = userList.size(); for (int i=0;i<len;i++){ TUser user = (TUser)userList.get(i); System.out.println("User Name:"+user.getName()); } System.out.println("\nStart query by iterate ……\n"); Iterator it = session.iterate(hql,new Integer(18),Hibernate.INTEGER); while (it.hasNext()){ TUser user = (TUser)it.next(); System.out.println("User Name:"+user.getName()); }

可以看到,这段代码实际上是将之前的find和iterate方法联用,根据同一条件进行查询。屏幕输出

如下: Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age,

tuser0_.version as version from t_user tuser0_ where (age>? ) User Name:Emma User Name:Sammi Start query by iterate…… Hibernate: select tuser0_.id as x0_0_ from t_user tuser0_ where (age>? ) User Name:Emma User Name:Sammi

注意“Start query by iterate…”之后的输出,这部分是由iterate方法执行所引发的操作日志。

Page 35: Hibernate的高级操作

这里,Hibernate只执行了一次SQL,即从库表中取出所有满足条件的记录id。前面iterate方法运行

过程中根据id查询记录的两条SQL语句并没有执行。

这其中的差异就在于Hibernate缓存机制。

find方法将执行Select SQL从数据库中获得所有符合条件的记录并构造相应的实体对象,实体对象

构建完毕之后,就将其纳入缓存。

这样,之后iterate方法执行时,它首先执行一条Select SQL以获得所有符合查询条件的数据id,随

即,iterate方法首先在本地缓存中根据id查找对应的实体对象是否存在(类似Session.load方法),如果缓

存中已经存在对应的数据,则直接以此数据对象作为查询结果,如果没找到,再执行相应的Select语

句获得对应的库表记录(iterate方法如果执行了数据库读取操作并构建了完整的数据对象,也会将其

查询结果纳入缓存)。

find方法将读取的数据纳入缓存,为之后的iterate方法提供了现成的可用数据,于是出现了上面这

种情况。

再执行以下代码: String hql = "from TUser where age > ?"; List userList = session.find(hql,new Integer(18),Hibernate.INTEGER); int len = userList.size(); for (int i=0;i<len;i++){ TUser user = (TUser)userList.get(i); System.out.println("User Name:"+user.getName()); } System.out.println("\nStart 2nd list……\n"); userList = session.find(hql,new Integer(18),Hibernate.INTEGER); len = userList.size(); for (int i=0;i<len;i++){ TUser user = (TUser)userList.get(i); System.out.println("User Name:"+user.getName()); }

观察日志:

Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age, tuser0_.version as version from t_user tuser0_ where (age>? ) User Name:Emma User Name:Sammi Start 2nd list…… Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age, tuser0_.version as version from t_user tuser0_ where (age>? ) User Name:Emma User Name:Sammi

两次 find 方法的重复执行并没有减少 SQL 的执行数量,这里缓存机制似乎并没有产生效果。

道理很简单,我们进行 find 数据查询时,即使缓存中已经有一些符合条件的实体对象存在,我们

也无法保证这些数据就是库表中所有符合条件的数据。假设第一次查询条件是 age>25,随即缓存中就

包括了所有 age>25 的 user 数据;第二次查询条件为 age>20,此时缓存中虽然包含了满足 age>25 的

数据,但这些并不是满足条件 age>20 的全部数据。

因此,find 方法还是需要执行一次 Select SQL 以保证查询结果的完整性(iterate 方法通过首先查

询获取所有符合条件记录的 id,以此保证查询结果的完整性)。

因此,find 方法实际上无法利用缓存,它对缓存只写不读。而 iterate 方法则可以充分发挥缓存带

Page 36: Hibernate的高级操作

来的优势,如果目标数据只读或者读取相对较为频繁,通过这种机制可以大大减少性能上的损耗。

这是基于充分利用缓存以提升性能上的考量。

同时,另外一方面,还有内存使用上的考虑。

假设我们需要对海量数据进行操作,那么,find 方法将一次获得所有的记录并将其读入内存。假

设有 10 万条符合查询条件的记录,那么,这 10 万条数据会被一次性读入,无疑这将带来极大的内存

消耗,此时很可能会触发 OutOfMemoryError,从而导致系统异常。

此时,解决方案之一就是结合 iterate 方法和 evict 方法逐条对记录进行处理,将内存消耗保持在

可以接受的范围之内,如: String hql = "from TUser where age > ?"; Iterator it = session.iterate(hql,new Integer(18),Hibernate.INTEGER); while (it.hasNext()){ TUser user = (TUser)it.next(); //将对象从一级缓存中移除

session.evict(user);

//二级缓存可以设定最大数据缓存数量,达到峰值时会自动对缓存中的较老数据 //进行废除,但是我们这里还是通过编码指定将对象从二级缓存中移除,这有助 //保持缓存的数据有效性 sessionFactory.evict( TUser.class, user.getId()); System.out.println("User Name:"+user.getName()); }

注意上面代码中的下画线部分,我们通过 Session/SessionFactory.evict 方法将数据对象强制从缓存

中移除,如果遗漏了这步操作,那么 user 对象实际上还是会被放入缓存中,那么当循环结束时,所有

符合条件的记录依然会充斥着缓存,这与 find 方法导致的结果相同。通过不断的读取,不断的释放,

我们就可以将可用内存数量维持在比较稳定的范围之内。

实际应用开发中,上面的方案也只能解决部分问题,由于 JVM 的异步内存回收机制,无效对象

会不断在内存中积累等待回收,如果数据量较大,必然频繁激发 JVM 的内存回收机制,导致系统性

能急剧下降。因此,实际开发中,对于大批量数据处理,还是推荐采用 SQL 或存储过程实现,以获

得较高的性能,并保证系统平滑运行。

Query Cache

前面讨论 Session.find 方法时,曾经有这样的分析:

我们进行 find 数据查询时,即使缓存中已经有一些符合条件的实体对象存在,我们也无法保证这

些数据就是库表中所有符合条件的数据。假设第一次查询条件是 age>25,随即缓存中就包括了所有

age>25 的 user 数据;第二次查询条件为 age>20,此时缓存中虽然包含了满足 age>25 的数据,但这些

并不是满足条件 age>20 的全部数据。

是的,对于这样的情况,我们不得不发起一次 Select SQL 以保证获取所有符合条件的记录。

但是,如果之前曾经有完全相同的查询条件出现,如已经发生过 age>20 的查询,那么第二次发

起 age>20 的查询时,我们是否可以利用前一个查询所产生的缓存数据?

Query Cache 正是为了解决这个问题而诞生的。

Query Cache 中保存了之前查询操作执行过的 Select SQL,以及由此查询产生的查询结果集(包

括查询对象的类型和 id)。

之后发生查询请求的时候,Hibernate 会首先根据查询的 SQL 从 Query Cache 中检索,如果此 SQL

曾经执行过,则取出对应这个 SQL 的检索结果集,再根据这个结果集中的对象类型及其 id,从缓存

Page 37: Hibernate的高级操作

中取出对应的实体对象返回。

Query Cache 中缓存的 SQL 及其结果集并非永远存在,当 Hibernate 发现此 SQL 对应的库表发生

了变动(Update/Delete/Insert),会自动将 Query Cache 中对应表的 SQL 缓存废除。因此,Query Cache

只在特定的情况下产生作用:

1. 完全相同的 Select SQL 重复执行。

2. 在两次查询之间,此 Select SQL 对应的库表没有发生过改变。

由于以上两个条件的严格限制,Query Cache 在实际应用中的意义并没有我们想象中的那么重大,

因此,Hibernate 在默认情况下也关闭了这个特性。

聊胜于无,对于一些特殊应用,这个特性还是有一些利用价值的,我们下面就来看看 Query Cache

的具体应用。

首先,为了启用 Query Cache,我们必须在 Hibernate 配置文件(hibernate.cfg.xml)中打开

hibernate.cache.use_query_cache 选项: <hibernate-configuration> <session-factory> …… <property name="hibernate.cache.use_query_cache">true</property> …… </session-factory> </hibernate-configuration>

之后我们必须在 Query 的查询执行之前,将 Query.Cacheable 设为 true: Query query = session.createQuery(hql).setInteger(0,20); query.setCacheable(true);

运行以下代码: String hql = "from TUser where age > ?"; Query query = session.createQuery(hql).setInteger(0,20); query.setCacheable(true); List userList =query.list(); int len = userList.size(); for (int i=0;i<len;i++){ TUser user =(TUser)userList.get(i); System.out.println(user.getName()); } System.out.println("\nSecond Query..."); query = session2.createQuery(hql).setInteger(0,20); query.setCacheable(true); //第二次查询时,也必须将Cacheable设为true userList =query.list(); len= userList.size(); for (int i=0;i<len;i++){ TUser user =(TUser)userList.get(i); System.out.println(user.getName()); }

观察输出: Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age, tuser0_.version as version from T_User tuser0_ where (age>? ) Sammi Luna Second Query... Sammi Luna

看到,第二次查询期间,Hibernate 并没有执行任何 Select SQL 即完成了任务,这就是 Query Cache 的

Page 38: Hibernate的高级操作

作用。

延迟加载(Lazy Loading)

为了避免在某些情况下,关联关系所带来的无谓的性能开销。Hibernate引入了延迟加载的概念。

如,之前示例中TUser对象在加载的时候,在非“延迟加载”的情况下,会同时读取其所关联的

多个地址(address)对象,对于确实需要对address进行操作的应用逻辑而言,关联数据的自动加载机

制的确非常有效。

但是,如果我们只是想要获得user的年龄(age)属性,而不关心user的地址(address)信息,那

么自动加载address的特性就显得多余,并且造成了极大的性能浪费。为了获得user的性别属性,我们

可能还要同时从数据库中读取数条无用的地址数据,这导致了大量无谓的系统开销。

延迟加载特性的出现,正是为了解决这个问题。

所谓延迟加载,就是在需要数据的时候,才真正执行数据加载操作。

Hibernate 2中的延迟加载实现主要针对:

1. 实体对象。

2. 集合(Collection)。

Hibernate 3 同时提供了属性的延迟加载功能。

实体对象的延迟加载

在关于Session.get/load方法的描述中,我们曾经提到,通过load方法我们可以指定可以返回目标

实体对象的代理。

而这个代理,在这个过程中起着怎样的角色?这正是下面我们需要探讨的话题。

首先我们来看正常情况下,一个非延迟加载的例子:

<hibernate-mapping> <class name="com.redsaga.hibernate.db.entity.TUser" table="T_USER" dynamic-update="false" dynamic-insert="false" select-before-update="false" optimistic-lock="version" lazy="false"4 > …… </hibernate-mapping>

TUser user = (TUser)session.load(TUser.class,new Integer(1)); ⑴ System.out.println(user.getName()); ⑵

首先观察代码运行至⑴后的 user 对象状态,user 内存快照如下(图 5-6):

4 Hibernate 2中,lazy属性默认为false,而Hiberante3中,其默认值为true。

Page 39: Hibernate的高级操作

图 5-6 Lazy=”false”时的内存快照

同时日志如下:

Hibernate: select tuser0_.id as id0_, tuser0_.name as name0_, tuser0_.age as age0_, tuser0_.version as version0_ from T_USER tuser0_ where tuser0_.id=?

可以看出,当程序运行到⑴时,Hibernate已经从库表中取出了对应的记录,并构造了一个完整的

TUser对象。

那么,使用了延迟加载机制之后,情况会是怎样?

为了使用实体的延迟加载功能,我们对以上映射配置修改如下: <hibernate-mapping> <class name="com.redsaga.hibernate.db.entity.TUser" table="T_USER" dynamic-update="false" dynamic-insert="false" select-before-update="false" optimistic-lock="version" lazy="true" > …… </hibernate-mapping>

通过class的lazy属性,我们可以打开实体对象的延迟加载功能。

那么,延迟加载具有怎样的特性?尝试在Eclipse Debug视图中观察以下代码的运行情况:

TUser user = (TUser)session.load(TUser.class,new Integer(1)); ⑴ System.out.println(user.getName()); ⑵

首先来看代码运行至⑴后的user对象状态(图5-7):

Page 40: Hibernate的高级操作

图5-7 user对象的内存快照

可以看到,此时的 user对象与我们之前定义的实体类并不相同,其当前类型描述为

TUser$EnhancerByCGLIB$$bede8986。且其属性均为null.

同时,观察屏幕日志,此时并没有任何Hiberate SQL输出,也就意味着,当我们获得user对象引

用的时候,Hibernate并没有执行数据库查询操作。

代码运行至⑵,再次观察user对象状态(如图5-8所示)。

图 5-8 代码运行至(2)时的内存状态

看到user对象的name属性仍然是null,但是观察屏幕输出,我们看到: Hibernate: select tuser0_.id as id0_, tuser0_.name as name0_, tuser0_.age as age0_, tuser0_.version as version0_ from T_USER tuser0_ where tuser0_.id=? Emma

查询操作已经执行,同时user.name属性也正确输出。

两次查询操作为什么会有这样的差异?

Page 41: Hibernate的高级操作

原因就在于Hibernate的代理机制。

Hibernate中引入了CGLib作为代理机制实现的基础。这也就是为什么我们会获得一个诸如

TUser$EnhancerByCGLIB$$bede8986类型对象的缘由。

CGLib可以在运行期动态生成Java Class。这里的代理机制,其基本实现原理就是通过由CGLib构

造一个包含目标对象所有属性和方法的动态对象(相当于动态构造目标对象的一个子类)返回,并以

之作为中介,为目标对象提供更多的特性。

从上面的内存快照可以看到,真正的TUser对象位于代理类的CGLIB$CALLBACK_0.target属性

中。

当我们调用user.getName方法时,调用的实际上是CGLIB$CALLBACK_0.getName()方法,当

CGLIB$CALLBACK_0.getName()调用后,它会首先检查CGLIB$CALLBACK_0.target中是否存在目标对象。

如果存在,则调用目标对象的getName方法返回,如果目标对象为空,则发起数据库查询指令,

读取记录、构建目标对象并将其设入CGLIB$CALLBACK_0.target。

这样,通过一个中间代理,实现了数据延迟加载功能,只有当客户程序真正调用实体类的取值方

法时,Hibernate才会执行数据库查询操作。

集合类型的延迟加载

Hibernate延迟加载机制中,关于集合的延迟加载特性意义 为重大,也是实际应用中相当重要的

一个环节。

回到开篇提到的一个例子:

如,之前示例中TUser对象在加载的时候,在非“延迟加载”的情况下,会同时读取其所关联的

多个地址(address)对象,对于确实需要对address进行操作的应用逻辑而言,关联数据的自动加载机

制的确非常有效。

但是,如果我们只是想要获得user的年龄(age)属性,而不关心user的地址(address)信息,那

么自动加载address的特性就显得多余,并且造成了极大的性能浪费。为了获得user的性别属性,我们

可能还要同时从数据库中读取数条无用的地址数据,这导致了大量无谓的系统开销。

对于我们这里TUser对象的加载过程,如果要做到集合的延迟加载,也就意味着,加载TUser对象

时只针对其本身的属性,而当我们需要获取TUser对象所关联的address信息时(如执行

user.getAddresses时),才真正从数据库中加载address数据并返回。

我们将前面一对多关系中的lazy属性修改为true,即指定了关联对象采用延迟加载:

<hibernate-mapping> <class name="org.hibernate.sample.TUser" table="t_user" dynamic-update="true" dynamic-insert="true" > …… <set name="addresses" table="t_address" lazy="true" ★ inverse="false" cascade="all" sort="unsorted" order-by="zipcode asc" > <key

Page 42: Hibernate的高级操作

column="user_id" > </key> <one-to-many class="org.hibernate.sample.TAddress" /> </set> …… </class> </hibernate-mapping>

尝试执行以下代码:

Criteria criteria = session.createCriteria(TUser.class); criteria.add(Expression.eq("name","Erica")); List userList = criteria.list(); TUser user =(TUser)userList.get(0); System.out.println("User name => "+user.getName()); Set hset = user.getAddresses(); session.close();//关闭Session TAddress addr = (TAddress)hset.toArray()[0]; System.out.println(addr.getAddress());

运行时抛出异常:

LazyInitializationException - Failed to lazily initialize a collection - no session or session was closed

如果我们稍做调整,将session.close放在代码末尾,则不会发生这样的问题。

这意味着,只有我们实际加载user关联的address时,Hibernate才试图通过session从数据库中加载

实际的数据集,而由于我们读取address之前已经关闭了session,所以出现了以上的错误。

这里有个问题,如果我们采用了延迟加载机制,但希望在一些情况下,实现非延迟加载时的功能,

也就是说,我们希望在Session关闭后,依然允许操作user的addresses属性。如,为了向View层提供数

据,我们必须提供一个完整的User对象,包含其所关联的address信息,而这个User对象必须在Session

关闭之后仍然可以使用。

Hibernate.initialize方法可以强制Hibnerate立即加载关联对象集: Hibernate.initialize(user.getAddresses()); session.close(); //通过Hibernate.initialize方法强制读取数据 //addresses对象即可脱离session进行操作 Set hset= user.getAddresses(); TAddress addr = (TAddress)hset.toArray()[0]; System.out.println(addr.getAddress());

为了实现透明化的延迟加载机制,Hibernate进行了大量努力。其中包括JDK Collection接口的独立

实现(参见稍后关于Collection的讨论)。

如果我们尝试用HashSet强行转化Hibernate返回的Set型对象:

Set hset = (HashSet)user.getAddresses();

就会在运行期得到一个java.lang.ClassCastException,实际上,此时返回的是一个Hibernate的特定

Set实现“net.sf.hibernate.collection.Set”,而非传统意义上的JDK Set实现。

Page 43: Hibernate的高级操作

这也正是我们为什么在编写POJO时,必须用JDK Collection Interface(如Set,Map),而非特定的

JDK Collection实现类(如HashSet、HashMap)声明Collection型属性的原因(如private Set addresses; 而非private HashSet addresses)。

回到前面TUser类的定义: public class TUser implements Serializable { …… private Set addresses = new HashSet(); …… }

我们通过Set接口,声明了一个addresses属性,并创建了一个HashSet作为addresses的初始实例,

以便我们创建TUser实例后,就可以为其添加关联的address对象: TUser user = new TUser(); TAddress addr = new TAddress(); addr.setAddress("Hongkong"); user.getAddresses().add(addr); session.save(user);

此时,这里的addresses属性是一个HashSet对象,其中包含了一个address对象的引用。

前面的“脏数据检查”部分中,我们讨论过针对无关联实体的保存。那么,在现在的情况下,当

调用session.save(user)时,Hibernate如何处理其关联的Addresses对象集?

通过Eclipse的Debug视图,我们可以看到session.save方法执行前后user对象发生的变化(图5-9):

图 5-9 session.save 方法之前的 user 对象快照与之后的快照

可以看到,user对象在通过Hibernate处理之后已经发生了变化。

首先,由于insert操作,Hibernate获得数据库产生的id值(在我们的例子中,采用native方式的主

键生成机制),并填充到user对象的id属性。这个变化比较容易理解。

另一方面,Hibernate使用了自己的Collection实现“net.sf.hibernate.collection.Set”对user中的HashSet

型addresses属性进行了替换,并用数据对其进行填充,保证新的addresses与原有的addresses包含同样

的实体元素。

再来看下面的代码: TUser user = (TUser)session.load(TUser.class,new Integer(1));

Page 44: Hibernate的高级操作

Collection addSet = user.getAddresses(); ⑴ Iterator it = addSet.iterator(); ⑵ while(it.hasNext()){ TAddress addr = (TAddress)it.next(); System.out.println(addr.getAddress()); }

根据之前的讨论我们知道,当代码执行到⑴处时, addresses 数据集尚未读入,我们得到的 addSet

对象实际上只是一个未包含任何数据的 net.sf.hibernate.collection.Set 实例。

代码运行至⑵,真正的数据读取操作才开始执行。观察一下 net.sf.hibernate.collection.Set.iterator

方法我们可以看到: public Iterator iterator() { read(); return new IteratorProxy( set.iterator() ); }

直到此时,真正的数据加载(read()方法)才开始执行。

Read 方法将首先在缓存中查找是否有符合条件的数据索引。

注意这里数据索引的概念,Hibernate 在对集合类型进行缓存时,分两部分保存,首先是这个集合

中所有实体的 id 列表(也就是所谓的数据索引,对于这里的例子,数据索引中包含了所有 userid=1

的 address 对象的 id 清单),其次是各个实体对象。

如果没有发现对应的数据索引,则执行一条 Select SQL(对于本例就是 select … from t_address

where user_id=?)获得所有符合条件的记录,接着构造实体对象和数据索引后返回。实体对象和数据

索引也同时被分别纳入缓存。

另一方面,如果发现了对应的数据索引,则从这个数据索引中取出所有 id 列表,并根据 id 列表

依次从缓存中查询对应的 address 对象,如果找到,则以缓存中的数据返回,如果没找到当前 id 对应

的数据,则执行相应的Select SQL获得对应的 address记录(对于本例就是 select … from t_address where

id=?)。

这里引出了另外一个性能关注点,即关联对象的缓存策略。

如果我们为某个集合类设定了缓存,如: <set name="addresses" table="t_address" lazy="true" inverse="true" cascade="all" sort="unsorted" > <cache usage="read-only"/> <key column="user_id"/> <one-to-many class="com.redsaga.hibernate.db.entity.TAddress" /> </set>

注意这里的<cache usage="read-only"/>只会使得 Hibernate 对数据索引进行缓存,也就是说,这

里的配置实际上只是缓存了集合中的数据索引,而并不包括这个集合中的各个实体元素。

执行下面的代码: TUser user = (TUser)session.load(TUser.class,new Integer(1));

Page 45: Hibernate的高级操作

Collection addSet = user.getAddresses(); //第一次加载user.addresses Iterator it = addSet.iterator(); while(it.hasNext()){ TAddress addr = (TAddress)it.next(); System.out.println(addr.getAddress()); } System.out.println("\n=== Second Query ===\n"); TUser user2 = (TUser)session2.load(TUser.class,new Integer(1)); Collection addSet2 = user2.getAddresses(); //第二次加载user.addresses Iterator it2 = addSet2.iterator(); while(it2.hasNext()){ TAddress addr = (TAddress)it2.next(); System.out.println(addr.getAddress()); }

观察屏幕日志输出: Hibernate: select … from T_User tuser0_ where tuser0_.id=? Hibernate: select … from T_Address addresses0_ where addresses0_.user_id=? Guangzhou Shanghai Beijing === Second Query === Hibernate: select… from T_Address taddress0_ where taddress0_.id=? Hibernate: select …from T_Address taddress0_ where taddress0_.id=? Hibernate: select …from T_Address taddress0_ where taddress0_.id=? Guangzhou Shanghai Beijing

看到,第二次获取关联的 addresses 集合的时候,执行了 3 次 Select SQL。

正是由于<set…><cache usage="read-only"/>…</set>的设定,第一次 addresses 集合被加载之后,

数据索引已经被放入缓存。

第二次再加载 addresses 集合的时候,Hibernate 在缓存中发现了这个数据索引,于是从索引里面

取出当前所有的 id(此时数据库中有 3 条符合的记录,所以共获得 3 个 id),然后依次根据这 3 个 id

在缓存中查找对应的实体对象,但是没有找到,于是发起了数据库查询,由 Select SQL 根据 id 从

t_address 表中读取记录。

我们看到,由于缓存中数据索引的存在,似乎 SQL 执行的次数更多了,这导致第二次借助缓存

的数据查询比第一次性能开销更大。

导致这个问题出现的原因何在?

这是由于我们只为集合类型配置了缓存,这样 Hibernate 只会缓存数据索引,而不会将集合中的

实体元素同时也纳入缓存。

我们必须为集合类型中的实体对象也指定缓存策略,如: <hibernate-mapping> <class name="com.redsaga.hibernate.db.entity.TAddress" table="T_Address" dynamic-update="false" dynamic-insert="false" select-before-update="false" optimistic-lock="version" > <cache usage="read-write"/>

Page 46: Hibernate的高级操作

…… </hibernate-mapping >

此时,Hibernate 才会对集合中的实体也进行缓存。

再次运行之前的代码,得到以下日志输出: Hibernate: select addresses0_.user_id as user_id__, addresses0_.id as id__,

addresses0_.id as id0_, addresses0_.address as address0_, addresses0_.zipcode as zipcode0_, addresses0_.tel as tel0_, addresses0_.type as type0_, addresses0_.user_id as user_id0_, addresses0_.idx as idx0_ from T_Address addresses0_ where addresses0_.user_id=?

Guangzhou Shanghai Beijing Second Query Guangzhou Shanghai Beijing

可以看到,第二次查询没有执行任何 SQL 即宣告完成,所有的数据都来自缓存,这无意对性能

的提升有着极其重要的意义。

上 面 我 们 探 讨 了 net.sf.hibernate.collection.Set.iterate 方 法 , 同 样 , 观 察

net.sf.hibernate.collection.Set.size/isEmpty 方法或者其他 hibernate collection 中的同类型方法实现,我们

可以看到同样的处理方式。

通过自定义 Collection 类型实现数据延迟加载的原理也就在于此。

这样,通过自身的 Collection 实现,Hibernate 就可以在 Collection 层从容的实现延迟加载特性。

只有程序真正读取这个 Collection 的内容时,才激发底层数据库操作,这为系统的性能提供了更加灵

活的调整手段。

属性的延迟加载

在前面的内容中,我们讨论了关于实体,及其关联集合对象的延迟加载机制。这些机制为改进持

久层性能表现提供了一个重要渠道。

根据我们已有的经验来看,上面这两种延迟加载模式,实质上都是面向数据实体。我们可以决定

是否即刻加载某个实体,或者某个实体集合。

如果需要对实体的某个部分(如某个属性)应用延迟加载策略,我们应如何入手?在基础篇中,

我们曾经探讨了有关实体粒度设计的主题。通过对同一库表建立不同粒度的实体映射关系,我们可以

变通的实现库表的部分加载,不过,这并非我们这里所说的延迟加载,另一方面,这样需要付出大量

的额外工作。

另外,我们也可以在 HQL 中通过 Select 子句限定加载的属性列表。不过,随之而来 HQL 语句的

琐碎语法实在令人厌倦。

在 Hibernate 2 中,为了避免实体整体加载可能带来的性能浪费,我们只能采取以上两种策略。

Hibernate 团队显然也意识到了这个问题,随即在 Hibernate 3 中针对这一功能做了强有力的补充。这

也就是下面我们所要探讨的主题:属性的延迟加载。

基础篇中我们曾经提及属性延迟加载的配置方式,通过 property 节点的 lazy 属性,我们为特定属

性指定延迟加载策略。

假设 T_User 表中存在一个长文本类型的 Resume 字段,此字段中保存了用户的简历数据。长文

本字段的读取相对而言会带来较大的性能开销,因此,我们决定将其设为延迟加载,只有真正需要处

Page 47: Hibernate的高级操作

理简历信息时,才从库表中读取。

首先,修改映射配置文件,将 Resume 字段的 lazy 属性设置为 true: <hibernate-mapping> <class name="com.redsaga.hibernate.db.entity.TUser" table="T_USER" batch-size="5" > <id name="id" column="id" type="java.lang.Integer" > <generator class="native"/> </id> <property name="name" type="java.lang.String" column="name" /> <property name="age" type="java.lang.Integer" column="age" /> <property name="resume" type="java.lang.String" column="resume" lazy="true" /> </class> </hibernate-mapping>

与实体和集合类型的延迟加载不同。Hibernate 3 属性延迟加载机制在配置之外,还需要借助类增

强器对二进制 Class 文件进行强化处理(buildtime bytecode instrumentation)。

在这里,我们通过 Ant 调用 Hibernate 类增强器对 TUser.class 文件进行强化处理。Ant 脚本如下: <project name="HibernateSample" default="instrument" basedir="."> <property name="lib.dir" value="./lib"/> <property name="classes.dir" value="./bin"/> <path id="lib.class.path"> <fileset dir="${lib.dir}"> <include name="**/*.jar"/> </fileset> </path> <target name="instrument"> <taskdef name="instrument"

classname="org.hibernate.tool.instrument.InstrumentTask"> <classpath path="${classes.dir}"/> <classpath refid="lib.class.path"/> </taskdef> <instrument verbose="true"> <fileset dir="${classes.dir}/com/redsaga/hibernate/db/entity"> <include name="TUser.class"/> </fileset> </instrument> </target> </project>

Page 48: Hibernate的高级操作

使用这段脚本时需要注意各个路径的配置。本例中,此脚本位于 Eclipse 项目的根目录下,./bin 为 Eclipse

的默认编译输出路径,./lib 下存放了执行所需的 jar 文件(hibernate3.jar 以及 Hibernate 所需的类库)。

以上 Ant 脚本将对 TUser.class 文件进行强化,如果对其进行反编译,我们可以看到如下内容: package com.redsaga.hibernate.db.entity; import java.io.Serializable; import java.util.Set; import net.sf.cglib.transform.impl.InterceptFieldCallback; import net.sf.cglib.transform.impl.InterceptFieldEnabled; public class TUser implements Serializable, InterceptFieldEnabled { public InterceptFieldCallback getInterceptFieldCallback() { return $CGLIB_READ_WRITE_CALLBACK; } public void setInterceptFieldCallback(InterceptFieldCallback

interceptfieldcallback) { $CGLIB_READ_WRITE_CALLBACK = interceptfieldcallback; } …略… public String $cglib_read_resume() { resume; if($CGLIB_READ_WRITE_CALLBACK != null) goto _L2; else goto _L1 _L1: return; _L2: String s; s; return (String)$CGLIB_READ_WRITE_CALLBACK.readObject(this,

"resume", s); } public void $cglib_write_resume(String s) { resume = $CGLIB_READ_WRITE_CALLBACK == null ? s :

(String)$CGLIB_READ_WRITE_CALLBACK.writeObject(this, "resume", resume, s);

} …略… }

可以看到,TUser 类的内容已经发生了很大变化。其间,cglib 相关代码被大量植入,通过这些代

码,Hibernate 在运行期即可截获 TUser 类的方法调用,从而为延迟加载机制提供实现的技术基础。

经过以上处理,运行以下测试代码: String hql ="from TUser user where user.name='Erica'"; Query query = session.createQuery(hql); List list = query.list();// this.session.createQuery(hql); Iterator it = list.iterator(); while(it.hasNext()){ TUser user = (TUser)it.next(); System.out.println(user.getName()); System.out.println(user.getResume()); }

观察输出日志: Hibernate: select … from T_USER tuser0_ where (tuser0_.name='Erica')

Page 49: Hibernate的高级操作

Erica Hibernate: select tuser_.resume as resume0_ from T_USER tuser_ where tuser_.id=? This is my resume.

可以看到,在此过程中,Hibernate 先后执行了两条 SQL,第一条 SQL 语句用于读取 TUser 中非

延迟加载的字段。而之后,当 user.getResume()方法调用时,随即调用第二条 SQL 从库表中读取 Resume

字段数据。属性的延迟加载已经实现。

数据保存

Session.save

Session.save 方法用于实体对象到数据库的持久化操作。也就是说,Session.save 方法调用与实体

对象所匹配的 Insert SQL,将数据插入库表。

还是结合一个简单实例来进行讨论: TUser user = new TUser(); user.setName("Luna"); Transaction tx = session.beginTransaction(); session.save(user); tx.commit();

首先,我们创建了一个 user 对象,并启动事务,之后调用 Session.save 方法对对象进行保存。

Session.save 方法中包含了以下几个主要步骤:

1. 在 Session 内部缓存中寻找待保存对象

内部缓存命中,则认为此数据已经保存(执行过 insert 操作),实体对象已经处于 Persistent

状态,直接返回。

此时,即使数据相对之前状态已经发生了变化,也将在稍后的事务提交时,由脏数据检查过程加

以判定,并根据判定结果决定是否要执行对应的 Update 操作。(参见“脏数据检查”部分的描述)

2. 如果实体类实现了 lifecycle 接口,则调用待保存对象的 onSave 方法。

3. 如果实体类实现了 Validatable 接口,则调用其 validate()方法

4. 调用对应拦截器的 Interceptor.onSave 方法(如果有的话)

5. 构造 Insert SQL,并加以执行

6. 记录插入成功,user.id 属性被设定为 insert 操作返回的新记录 id 值

7. 将 user 对象放入内部缓存

这里值得一提的是,save 方法不会把实体对象纳入二级缓存,因为通过 save 方法保存的实

体对象,在事务的剩余部分中被修改的几率往往很高,缓存的频繁更新以及随之而来的数

据同步问题的代价,已经超过了此数据得到重用的可能收益,得不偿失。

8. 后,如果存在级联关系,对级联关系进行递归处理。

Session.update

在前面的“实体对象的 3 种状态”部分,我们曾经探讨过实体对象从 Detached 状态到 Persistent

状态的转换。

我们首先来回顾一下这个示例: TUser user = new TUser(); user.setName("Emma"); //此时user处于Transient状态

Page 50: Hibernate的高级操作

Transaction tx = session.beginTransaction(); session.save(user); //user对象已经由Hibernate纳入管理容器,处于Persistent状态 tx.commit(); session.close(); //user对象此时状态为Detached,因为与其关联的session已经关闭 Transaction tx2 = session2.beginTransaction(); session2.update(user); //处于Detached状态的user对象再次借助session2由Hibernate纳入 //管理容器,恢复Persistent状态 user.setName("Emma_1"); //由于user对象再次处于Persistent状态,因此其属性变更将自动由 //Hibernate固化到数据库中 tx2.commit();

这里我们通过 update 方法将一个 Detached 状态的对象与 Session 重新关联起来,从而使之转变为

Persistent 状态。

那么 update 方法中,到底进行了怎样的操作完成这一步骤?

1. 首先,根据待更新实体对象的 Key,在当前 session 的内部缓存中进行查找,如果发现,则

认为当前实体对象已经处于 Persistent 状态,返回。

从这一点我们可以看出,对一个 Persistent 状态的实体对象调用 update 语句并不会产生任何

作用。

2. 初始化实体对象的状态信息(作为之后脏数据检查的依据),并将其纳入内部缓存。

注意这里 Session.update 方法本身并没有发送 Update SQL 完成数据更新操作,Update SQL

将在之后的 Session.flush 方法中执行(Transaction.commit 在真正提交数据库事务之前会调用

Session.flush)。

Session.saveOrUpdate

通过前面对 save 和 update 方法实现机制的探讨。相信 saveOrUpdate 方法的幕后原理大家已经猜

到一二:

1. 首先在 Session 内部缓存中进行查找,如果发现则直接返回。

2. 执行实体类对应的 Interceptor.isUnsaved 方法(如果有的话),判断对象是否为未保存状态。

3. 根据 unsaved-value 判断对象是否处于未保存状态。

4. 如果对象未保存(Transient 状态),则调用 save 方法保存对象。

5. 如果对象为已保存(Detached 状态),调用 update 方法将对象与 Session 重新关联。

可以看到,saveOrUpdate 实际上是 save 和 update 方法的组合应用。它本身并没有增加新的功能

特性,但是却为我们的应用层开发提供了一个相当便捷的功能选择。

实际开发中,我们常常通过接口约定业务逻辑层和持久层的交互方式。如 public interface IUserDAO { public TUser getUser(String id); public void saveUser(TUser user); }

业务逻辑开发人员将通过 getUser 方法获取用户数据,通过 saveUser 方法保存用户信息。当

saveUser 方法被调用的时候,问题就出现了,我们无法预先得知业务层传递过来的 user 对象处于怎样

一个状态。

Page 51: Hibernate的高级操作

下面是可能出现的两种情况: TUser user = new TUser(); user.setAge(28); //此时的user为Transient状态,我们应该在UserDAO.save方法中 //通过Session.save方法保存 UserDAO.save(user);

TUser user = userDAO.getUser("Erica"); user.setAge(28); //此时的user为Detached状态,我们应该在UserDAO.save方法中 //通过Session.update方法更新 UserDAO.save(user);

其中,UserDAO 是 IUserDAO 接口的实现,UserDAO.getUser 方法实现如下: public TUser getUser(String id) throws HibernateException{ Session session = getSession(); TUser user = (TUser)session.load(TUser.class,id); Session.close() return user; }

针对这两种情况我们该如何处理?

如果没有 Session.saveOrUpdate 方法,我们可能就不得不针对以上两种情况分别提供一个

createUser 和一个 updateUser 方法。

而有了 saveOrUpdate 方法,处理就相当简单明了,我们无需关心传入的 user 参数到底是怎样的

状态: public void saveUser(TUser user){ Session session = getSession(); try{ Transaction tx = session.beginTransaction(); session.saveOrUpdate(user); tx.commit(); }finally{ if (session!=null) session.close(); } }

数据批量操作

上面我们讨论了 Hibernate 中的数据保存操作,但是,可以看出,上面的讨论主要围绕着单个对

象的状态保存。那么,对于批量的数据插入、更新和删除操作,我们应该采取怎样的策略?

显然, 简单的方式就是通过迭代调用 Session.save/update/saveOrUpdate/delete 操作。从逻辑上而

言,这样的解决方式并没有什么问题。不过,从性能角度考虑,这样的做法却有待商榷。

下面的内容,我们就将围绕数据的批量操作这个主题进行探讨。

数据批量导入

在实际开发中,我们常常会碰到数据的批量导入需求。

举个简单的例子,我们需要导入 10 万个用户数据。那么,对应我们实现了相应的数据批量导入

方法: public void importUsers() throws HibernateException{ Transaction tx = session.beginTransaction(); for(int i=0;i<100000;i++){ TUser user = new TUser(); user.setName("user"+i); session.save(user);

Page 52: Hibernate的高级操作

} tx.commit(); }

代码从逻辑上看,并没有什么问题。但是运行期我们可能就会发现,程序运行由于

OutOfMemoryError 而异常中止。

为什么会出现这样的情况?

原因在于 Hibernate 内部缓存的维护机制,每次我们调用 Session.save 方法时,当前 session 都会将此

对象纳入自身的内部缓存进行管理(回忆之前关于 Session.save 方法的讨论)。

内部缓存与二级缓存不同,我们可以在二级缓存的配置中指定其 大容量,但内部缓存并没有这

样的限制。

随着循环的进行,越来越多的 TUser 实例被纳入到 Session 内部缓存之中,内存逐渐耗尽,于是

产生了 OutOfMemoryError。

如何避免这样的问题?

一个解决方案是每隔一段时间清空 Session 内部缓存,如: Transaction tx = session.beginTransaction(); for(int i=0;i<100000;i++){ TUser user = new TUser(); user.setName("user"+i); session.save(user); if (i%25==0){//以每25个数据作为一个处理单元 session.flush(); session.clear(); } } tx.commit();

通过阶段性调用 Session.clear()方法,我们可以将 Session 内部缓存所占用的空间维持在一个合理

的范围之内。

经过这样的处理,以上代码应该可以顺利执行完毕,我们成功地实现了所需的功能。

但是,这样也许还不足以让用户满意,对于 10 万条数据而言,导入过程必然消耗相当的时间。

固然,由于数据库插入操作必须的时间消耗,长时间的等待也许必不可免。不过,对于这里的情况,

有没有什么办法可以加以优化?

回忆以下,在传统 JDBC 编程时,对于批量操作,我们一般用怎样的方式加以优化?下面的代码

是一个典型的基于 JDBC 的改进实现: PreparedStatement stmt = conn.prepareStatement("INSERT INTO T_User (name)

VALUES(?)"); for(int i=0; i<10000; i++) { stmt.setString(1, "user"+i); stmt.addBatch(); }

int[ ] counts = stmt.executeBatch();

这里我们通过 PreparedStatement.executeBatch 方法,将数个 SQL 操作批量提交以获得性能上的

提升。

那么 Hibernate 中是否有对应的批量操作方式呢?

我们可以通过设置 hibernate.jdbc.batch_size 参数来指定 Hibernate 每次提交 SQL 的数量: <hibernate-configuration> <session-factory> ……

Page 53: Hibernate的高级操作

<property name="hibernate.jdbc.batch_size">25</property> …… </session-factory> </hibernate-configuration>

这样,当我们发起 SQL 调用的时候,Hibernate 会累积到 25 个 SQL 之后批量提交,从而实现了

与上面 JDBC 代码类似的效能。

同样的方法,也可以用于 Update 操作和 Delete 操作。

下面,我们做个简单的测试,看看 hibernate.jdbc.batch_size 参数对于批量插入操作的实际影响。 public void importUserList() throws HibernateException{ Transaction tx = session.beginTransaction(); for(int i=0;i<10000;i++){ TUser user = new TUser(); user.setName("user"+i); session.save(user); if (i%25==0){//以每25个数据作为一个处理单元 session.flush(); session.clear(); } } tx.commit(); } public void testBatchInsert(){ long startTime = System.currentTimeMillis(); try { this.importUserList(); } catch (HibernateException e) { e.printStackTrace(); } long currentTime = System.currentTimeMillis(); System.out.println("Batch Insert Time cost in ms => "+(currentTime - startTime));

}

测试环境:

操作系统:Windows XP Professional Sp2

JDK版本:Sun JDK1.4.2_08

CPU: P4 1.5G Mobile

RAM: 512MB

数据库: SQLServer 2000/Oracle 9i

JDBC: jtds JDBC Driver for SQLServer 1.02 /Oracle JDBC Driver 9.0.2.0.0

注:Mysql JDBC Driver 不支持 BatchUpdate 方式,因此 batch_size 的设定对 MySQL 无效。

在不同的 hibernate.jdbc.batch_size 设置下运行此代码,得到表 5-5 中的结果(数据库与测试程序

同在本机)。

表 5-5 测试结果

数据库 hibernate.jdbc.batch_size 耗时(ms)

SQLServer 2000 0 5979

SQLServer 2000 25 5047

Oracle 9i 0 16914

Oracle 9i 25 13820

(10 000条数据插入时间,重复3次,取平均数)

Page 54: Hibernate的高级操作

可能大家对 Oracle9i 的糟糕表现有点诧异,这一方面取决于测试机器的配置,Oracle 等重型企业级数

据库往往需要较高档次的运行环境。另一方面,本例中,TUser 类采用了 native 的主键生成机制,对于 Oracle

而言,则需要借助 Sequence 完成操作,这意味着 Hibernate 需要首先 Select Sequence 获得主键值,再执行

Insert 操作,也就是说每次保存操作实际上涉及了两次数据库访问,自然效率也相对降低。

作为横向对比,我们测试一下 JDBC 的表现(SQL Server 2000): PreparedStatement ps =conn.prepareStatement("INSERT INTO T_User (name) VALUES(?)");

for (int k = 0; k < 400; k++) { for (int i = 0; i < 25; i++) { ps.setString(1, "user"); ps.addBatch(); } ps.executeBatch(); conn.commit(); }

耗时1743ms。

而如果数据库与测试程序分别在两台机器上运行(百兆局域网,两台机器的软硬件配置均与上面

相同),结果如表 5-6 所示。

表 5-6 测试结果

数据库 hibernate.jdbc.batch_size 耗时(ms)

SQLServer 2000 0 16875

SQLServer 2000 25 6720

Oracle 9i 0 43763

Oracle 9i 25 35841

(10 000条数据插入时间,重复3次,取平均数)

可以看到,对于远程数据库,hibernate.jdbc.batch_size 的设定就相当关键。

这里的差距,并不是数据存取机制有什么不同,而是在于网络传输上的损耗,对于数据库与应用

均部署在本机的情况而言,数据通讯上的性能损耗较小,因而 hibernate.jdbc.batch_size 设定的影响相

对较弱,而对于远程数据库,网络传输上的损耗就不可不计,因而不同的传输模式(批量传输与单笔

传输)将对性能的整体表现产生较大影响。

数据批量删除

批量删除操作在 Hibernate2 和 Hibernate3 中有着不同的实现机制(Hibernate3 兼容 Hibernate2,同

时提供了更多的选择),首先我们来看 Hibernate2 中的批量删除。

下面是一段典型的 Hibernate2 批量删除代码: Transaction tx = session.beginTransaction(); session.delete("from TUser"); tx.commit();

(假设数据库 T_User 表中有 10 000 条记录)

对于这样的代码,Hibernate 会执行以下语句: select ... from T_User tuser0_

Page 55: Hibernate的高级操作

Hibernate: delete from T_User where id=? Hibernate: delete from T_User where id=? Hibernate: delete from T_User where id=? ……

Hibernate 会首先从数据库查询出所有符合条件的记录,再对此记录进行循环删除,实际上,

session.delete("from TUser")等价于: Transaction tx = session.beginTransaction(); List userList = session.find("from TUser"); int len = userList.size(); for (int i=0;i<len;i++){ session.delete(userList.get(i)); } tx.commit();

实际上,Hibernate 内部,Delete 方法的实现也正是如此,如下: public int delete(String query, Object[] values, Type[] types) throws HibernateException {

if ( log.isTraceEnabled() ) { log.trace( "delete: " + query ); if (values.length!=0) log.trace( "parameters: " +

StringHelper.toString(values) ); } List list = find(query, values, types); int size = list.size(); for ( int i=0; i<size; i++ ) delete( list.get(i) ); return size; }

看上去很难以理解的实现方式,为什么 Hibernate 不单独执行一条 Delete SQL “delete t_user where

id>5”完成所有的工作呢?

这也就是所有 ORM 框架都必须面对的问题。ORM 为了自动维持其内部状态属性,必须知道用

户到底对哪些数据进行了操作。它必须首先从数据库中获得所有待删除对象,才能根据这些对象,对

目前内部缓存和二级缓存中的数据进行整理,以保持内存状态与数据库数据的一致性。

如果单单执行一条“delete t_user where id>5”完成删除操作,那么,具体删除了哪些数据只有数

据库知道,ORM 并无法获知,因而极易导致内存数据与底层数据库之间的一致性受到破坏,下次用

户从缓存中读出的数据,很可能就是数据库中已经被删除的数据,从而导致严重的逻辑错误。

当然,解决的办法并不是没有,ORM 可以根据调用的 Delete SQL 对缓存中的数据进行处理,只

要是缓存中 TUser 对象的 id 值大于 5 的统统废除,缓存数据废除之后,再执行“delete t_user where

id>5”。但是,如此的需求将导致缓存的管理复杂性大大增加(实际上实现了一个支持 SQL 的内存

数据库),这样的要求对于一个轻量级 ORM 实现而言未免苛刻。

批量删除操作同样会遇到与数据批量导入操作同样的问题:

1) 内存消耗

Hibernate 在进行批量删除操作之前,首先必须将所有符合条件的数据加载到内存中,如果数据量

过大,就会导致 OutOfMemoryError。

对于内存消耗问题,无法像之前一样通过 Session.clear 操作解决,因为我们并无法干涉数据的批

量加载过程。

变通的方法之一:用 Session.iterate 或者Query.iterate 方法逐条获取数据,再执行 delete 操作。

Page 56: Hibernate的高级操作

另外,Hibernate 2.16 之后的版本提供了基于游标的数据遍历操作,为解决这个问题提供了一个较

好的解决方案(前提是所使用的 JDBC 驱动必须支持游标)。通过游标,我们可以逐条获取数据,从

而使得内存处于较为稳定的使用状态。

下面是基于游标的 Hibernate 批量删除示例: Transaction tx = session.beginTransaction(); String hql = "from TUser"; Query query = session.createQuery(hql); ScrollableResults scRes = query.scroll(); while(scRes.next()){ TUser user =(TUser)scRes.get(0); session.delete(user); } tx.commit();

2) 迭代删除操作的执行效率

由于 Hibernate 批量删除操作过程中,需要反复调用 delete SQL,因此同样存在 SQL 的批量发送

问题。对于这个问题,我们仍然采用调整 hibernate.jdbc.batch_size 参数解决。

表 5-7 是以上代码的批量删除测试结果。

表 5-7 测试结果

数据库 hibernate.jdbc.batch_size 耗时(ms)

SQL Server 2000 0 3726

SQL Server 2000 25 3285

Oracle 9i 0 5418

Oracle 9i 25 3125

(10 000条数据删除时间,重复3次,取平均数)

同样,我们用 JDBC 代码测试: String sqlStr = "delete from t_user"; Statement statement = dbconn.createStatement(); statement.execute(sqlStr);

耗时:390ms。

可以看到,即使是优化过的批量删除功能,性能差距还是相当可观的(近 10 倍的差距)。因此,

在 Hibernate2 中,对于批量操作而言,适当的时候采用传统的 JDBC 进行直接的批量数据库操作(此

时应特别注意对缓存的影响),可以获得性能上的极大提升,特别是对于批量性能关键的逻辑实现而

言。

为此牺牲的所谓设计上的优雅性,未必就那么令人惋惜。毕竟对于应用系统的开发而言,为客户

提供一个满足需求并且高效稳定的系统才是第一目标,产品 终能得到用户的欢迎,才是真正的优雅

☺。

考虑到以上问题,Hibernate3 HQL 语法中引入了 bulk delete/update 操作,Bulk delete/update 操作

的原理,即通过一条独立的 SQL 语句完成数据的批量删除/更新操作(类似上例中的 JDBC 批量删除)。

我们可以通过如下代码删除 T_User 表中的所有记录: Transaction tx = session.beginTransaction(); String hql ="delete TUser"; Query query = session.createQuery(hql); int ret = query.executeUpdate();

Page 57: Hibernate的高级操作

tx.commit(); System.out.println("Delete Records =>"+ret);

观察运行期日志输出: Hibernate: delete from T_USER Delete Records =>6

可以看到,通过一条干净利落的“delete from T_USER”语句,我们即完成数据的批量删除功能,

从底层实现来看,这与之前 JDBC 示例中的实现方式并没有什么不同,性能表现也大致相似。

那么,我们之前曾经谈及的批量删除与缓存管理上的矛盾,在 Hibernate3 中是否仍然存在?

这也正是我们必须特别注意的一点,Hibernate3 的 bulk delete/update 实际上仍然没有解决缓存同

步上的问题,无法保证缓存数据的一致有效性。

我们看以下示例:

//加载ID=1的用户记录 TUser user=(TUser)session.load(TUser.class,new Integer(1)); System.out.println("User name is ==> "+user.getName()); //删除ID=1的用户记录 Transaction tx = session.beginTransaction(); session.delete(user); tx.commit(); //尝试再次加载 user=(TUser)session.load(TUser.class,new Integer(1)); System.out.println("User name is ==> "+user.getName());

尝试运行以上代码,在尝试再次加载已删除的 TUser 对象时, Hibernate 将抛出

ObjectDeletedException,表明此对象已删除,加载失败。

将以上代码修改为通过 Bulk delete/update 删除的形式:

//加载ID=1的用户记录 TUser user=(TUser)session.load(TUser.class,new Integer(1)); System.out.println("User name is ==> "+user.getName()); //通过Bulk delete/update删除ID=1的用户记录 Transaction tx = session.beginTransaction(); String hql ="delete TUser where id=1"; Query query = session.createQuery(hql); query.executeUpdate(); tx.commit() //再次尝试加载 user=(TUser)session.load(TUser.class,new Integer(1)); System.out.println("User name is ==> "+user.getName());

输出日志如下: Hibernate: select tuser0_.id as id0_, tuser0_.name as name0_0_, tuser0_.age as

age0_0_ from T_USER tuser0_ where tuser0_.id=? User name is ==> ERICA Hibernate: delete from T_USER where id=1 User name is ==> ERICA

可以看到,第二次加载操作成功,由于缓存同步上的问题,我们得到了一个已被删除的过期数据

对象。

通过前面的讨论,我们知道,Hibernate 中维护了两级缓存:

1. 内部缓存(Session Level Cache)

在 Session 生命周期内存在,每个 Session 中都维护了一个独立缓存,为当前 Session 实例所独享。

Page 58: Hibernate的高级操作

2. 二级缓存(SessionFactory Level Cache)

由当前 SessionFactory 创建的多个 Session 实例共享。

上面的代码中,我们通过同一个 Session 实例反复进行数据加载,第二次查询操作将从内部缓存

中直接查找数据返回。

那么,在不同 Session 实例之间的协调情况如何,二级缓存中的数据有效性是否能得到保证?

打开 Hibernate 二级缓存,运行以下代码:

//加载ID=1的用户记录 TUser user=(TUser)session.load(TUser.class,new Integer(1)); System.out.println("User name is ==> "+user.getName()); //加载ID=1的用户记录已被放入二级缓存 //通过Bulk delete/update删除ID=1的用户记录 Transaction tx = session.beginTransaction(); String hql ="delete TUser where id=1"; Query query = session.createQuery(hql); query.executeUpdate(); tx.commit(); //通过另一Session实例再次尝试加载 user=(TUser)anotherSession.load(TUser.class,new Integer(1)); System.out.println("User name is ==> "+user.getName());

在尝试再次加载已删除数据对象时,我们调用了另一个 Session 实例 anotherSession。

运行日志输出如下: Hibernate: select ... from T_USER tuser0_ where tuser0_.id=? User name is ==> ERICA Hibernate: delete from T_USER where id=1 User name is ==> ERICA

可以看到,与前例相同,第二次数据加载时 Hibernate 依然返回了无效数据。

也就是说,bulk delete/update 只是提供了面向高性能批量操作的一种实现途径,但无法保证缓存

数据的一致有效性,在实际开发中,我们必须特别注意这一点,在缓存策略的制定上须特别谨慎。

在本书成书之时,Hibernate 新版本为 3.0.2,相信之后的 3.X 版本会进一步改进其缓存的管理

机制,读者在阅读本章时,如果已经有新版发布,可自行进行试验加以判断。

数据的批量更新与批量删除相关知识点基本相同,这里就不再赘述。

5.1.7 Collection

Collection 类型

在“Hibernate 基础”中我们已经接触了 Hibernate 的一对多,多对多映射关系。其中,Collection

扮演着数据容器的重要角色。

那么,在 Hibernate 中,支持几种类型的 Collection,它们之间的差异何在?这正是下面我们所要

探讨的主题。

Hibernate 中涉及的 Collection 类型共有以下几种:

无序集:Set,Bag,Map

有序集:List

由于传统的 Java Set、Map、List 实现不能满足要求,Hibernate 根据这些接口提供了自己的实现。

Page 59: Hibernate的高级操作

我们这里所说的 Set、Map 和 List,均指 Hibernate 中的实现版本。具体可参见 Hibernate 源代码中的

net.sf.hibernate.collection 包(Hibernate3 中对应为 org. hibernate.collection)。

这里所谓的无序和有序,是针对 Hibernate 数据持久过程中,是否保持数据集合中的记录排列顺

序加以区分的。

对于被定义为有序集的数据集合,Hibernate 在持久化过程中,会将集合中元素排列的先后顺序同

时固化到数据库中(以某个特定的字段储存顺序号),下次读取的时候,也会返回一个具备同样排列

顺序的数据集合。

下面我们分别就这 4 种类型的特点及其应用进行一些探讨。

1. Set

Hibernate Set 类型的实现位于 net.sf.hibernate.collection.Set。通过对 java.util.HashSet 的封装,它实

现了 java.util.Set 接口并进行了扩充。

Hibernate Set遵循 Java Collection中关于Set的定义,即集合中不允许出现两个相同元素。如: TUser user = new TUser(); user.setName("Emma"); set.add(user); set.add(user); System.out.println("Item Count in set=>"+set.size());

观察输出结果我们可以看到,虽然我们执行了两次 add 操作,Set 结构中仍然只包含了一个引用。

对于实体对象与表的映射关系而言,这样的机制一般并不会引发什么问题,Hibernate 在返回关联

对象集合的时候,会自动为每个记录构建一个实体对象。如“Hibernate 基础”部分的一对多映射模型

中,对应某个 t_user 表记录,有 3 个 t_address 记录,那么 Hibernate 就会构造 3 个 TAddress 对象,放

在 TUser 的 addresses 集合中返回。即使这 3 个 address 记录的逻辑内容完全相同(id 不同)。这个实

例对应的映射配置如下: …… <set name="addresses" table="t_address" lazy="false" > <key column="user_id"/> <one-to-many class="org.hibernate.sample.TAddress" /> </set> ……

但如果我们并不是通过实体对象(TAddress)进行映射,而是直接通过 String 对 t_address 表的

address 字段进行组合映射,就会触发一个陷阱。

如在 TUser.hbm.xml 中进行如下修改: …… <set name="addresses" lazy="true" table="t_address"> <key column="user_id"/> <element type="string" column="address"/> </set> ……

上面的配置,直接将 t_address 表的 address 字段与 TUser 相关联。TUser 对象的 addresses 集合由

来自 t_address 表对应记录的 address 字段(String 类型)填充。

再回忆一下 Java 语言基础中关于字符串比较的内容。

Page 60: Hibernate的高级操作

String str1="Hello"; String str2="Hello"; System.out.println(str1==str2);

运行上面的代码,我们将得到一个“true”的打印输出,这也就是说,str1 和 str2 在代码中虽然

看上去是两个字符串对象,其实却是同一个字符串的引用。

那么,我们上面的配置会产生怎样的结果?

显然,如果 t_address 表中对应记录的 address 字段内容相同,那么返回的 Addresses Set 集合中,

只会保留一个元素。 TUser user = (TUser)session.load(TUser.class,new Integer(1)); System.out.println( "Address Count=>"+user.getAddresses().size() );

可以看到,输出结果为 1。

本例中的库表数据如图 5-10 所示:

图 5-10 示例中的库表数据

查询如此,而数据删除也是同样的道理。Hibernate 会删除所有与指定记录内容相同的数据。如: TUser user = (TUser)session.load(TUser.class,new Integer(1)); Transaction tx = session.beginTransaction(); Object obj = user.getAddresses().iterator().next(); user.getAddresses().remove(obj); tx.commit(); session.flush();

hibernate 会执行如下 SQL:

delete from t_address where user_id=? and address=?

可见,只要 user_id 和 address 字段内容与条件符合,所有记录都会被抹去(而往往我们只想删除

某条特定的记录)。对于这里的地址数据而言,也许这样的现象不会造成太大的后果,但是对于一些

关键数据,则可能造成难以预料的逻辑错误。

在使用 Set 类型的集合类型前,请特别注意这个问题。

为了补充 Set 数据类型在这方面的限制,Hibernate 提供了 Bag 类型以供选用。

2. Bag

“Bag”类型在这里则比较特殊,它是 Hibernate 自定义的集合类型(Java 集合框架中并没有关于

Bag 的定义),实现了一个允许包含重复元素的“Set”。

Bag 的底层是借助一个 List 实现,但却屏蔽了 List 的有序特性,也就是说,通过“Bag”声明的

数据集合,其元素排列顺序将不会被持久化。

Page 61: Hibernate的高级操作

为了说明 Bag 的特性,我们对上面的例子进行一些修改: <bag name="addresses" lazy="true" table="t_address"> <key column="user_id"/> <element type="string" column="address"/> </bag>

再次运行如下代码(注意 TUser.addresses 属性需对应修改为 List 或者 Collection 类型)。 TUser user = (TUser)session.load(TUser.class,new Integer(1)); System.out.println( "Address Count=>"+user.getAddresses().size() );

可以看到,结果为 3。TUser.addresses 中包含了所有的数据记录。

Bag 集合为无序集,且允许出现重复元素,这也带来了一个问题。当删除某个元素的时候,我们

该如何定位这条待删记录?由于存在多个相同的元素,我们无法区分各个元素与数据库记录的对应关

系。

Bag 的实现方式,实际上是先将库表中原有的集合数据全部删除,再将现有数据逐条插入。无疑,

这种方式的数据更新的性能是及其低下的。

对于这种情况,如果集合中每个元素都拥有一个 id 可以惟一检索到对应的数据库记录,那么问

题就迎刃而解。

而 idbag,作为 Bag 的一种延伸,则成功地解决了这个问题。

idbag 配置比 Bag 多出了一项“collection-id”,用于配置 id 字段。根据此 id 字段,Hibernate 就可

以准确定位库表记录,从而实现高效的数据操作。 <idbag name="addresses" lazy="true" table="t_address"> <collection-id type="int" column="id"> <generator class="identity"/> </collection-id> <key column="user_id"/> <element type="string" column="address"/> </idbag>

类似类/表映射关系,这里我们也可以指定 id 生成机制。不过,在目前的版本中,尚不支持 native

类型的 id 生成机制。

3. Map

Map 同样是一个无序集合类型,与 Set/Bag 不同的是,Map 提供了键值对应关系。

一个典型的 Map 配置如下: <map name="addresses" lazy="true" table="t_address"> <key column="user_id"/> <index type="string" column="type" /> <element type="string" column="address"/> </map>

与 Set 配置相比,Map 增加了一个 index 配置,用于指定用作 Key 的字段名;此字段要求在数据

集中取值惟一。

之后我们即可在代码中通过键值对集合中的元素进行索引。 TUser user = (TUser) session.load(TUser.class, new Integer(1)); //读取家庭地址 System.out.println( "Home Address is:" + user.getAddresses().get("Home")); //读取办公地址 System.out.println( "Office Address is:" + user.getAddresses().get("Office"));

Page 62: Hibernate的高级操作

打印输出如下: Home Address is: Shanghai Office Address is: Beijing

示例库表数据如图 5-11 所示。

图 5-11 示例中的库表数据

4. List

与前面几种不同,List 实现了集合内元素顺序的持久化。与 Map 集合需要额外字段保存键值一样,

它要求库表必须配属对应的字段以保持次序信息。 <list name="addresses" lazy="true" table="t_address"> <key column="user_id"/> <index type="integer" column="idx" /> <element type="string" column="address"/> </list>

index 节点中,我们指定以“idx”字段保存次序状态。

下面的代码通过交换集合中两个元素的次序,演示了 List 集合元素次序的持久化。 TUser user = (TUser) session.load(TUser.class, new Integer(1)); Transaction tx = session.beginTransaction(); //第0和第2项交换位置后保存 Object addr0 = user.getAddresses().get(0); Object addr2 = user.getAddresses().get(2); user.getAddresses().set(0,addr2); user.getAddresses().set(2,addr0); tx.commit(); session.flush();

执行前的库表数据(图 5-12):

图 5-12 执行前的库表数据

执行后(图 5-13):

图 5-13 执行后的库表数据

Page 63: Hibernate的高级操作

5.1.8 结果集排序

之前关于 Collection 部分的讨论中,我们引入了所谓有序集和无序集的概念。无序集和有序集,

是针对 Hibernate 数据持久过程中,是否保持数据集合中的记录排列顺序加以区分的。

也就是说,对于一个有序集,其中元素的排列次序将会在库表中制定的字段中保存,而下次读取

时,也会以同样的次序排列。

而下面我们所要探讨的,则是关于 Collection 中的元素排序问题。

排序强调的是针对现有数据,以特定的逻辑对其排列次序进行调整。而排序的结果,是数据在内

存中的某种排列次序,属于临时状态。

数据排序有两种方式:

1. Sort

Collection 中的数据排序。如对一个 List 中的元素先后顺序进行调整。

2. order-by

对数据库执行 Select SQL 时,由 order by 子句实现的数据排序方式。

可以看出,这两种排序方式的 基本差异在于,Sort 操作是在 JVM 中完成。而 order-by 是由数

据库完成。

下面我们分别就这两种类型的排序方式进行探讨:

Sort

首先来看一个简单的示例: <set name="addresses" lazy="true" table="t_address" sort="natural"> <key column="user_id"/> <element type="string" column="address"/> </set>

可排序 Set 在 Hibernate 中对应的实现类为 net.sf.hibernate.collection.SortedSet,它实现了

java.util.SortedSet 接口。

sort="natural"指定采用 Java 默认排序机制,它会调用相应数据类型的 compareTo 方法进行排序中

的值比对。这里<element type="string"…>指定了元素类型为 string,也就是说,排序将基于

String.compareTo 方法。

如果期望指定某种特殊的排序算法,那么我们可以实现 java.util.Comparator 接口,并以此实现作

为排序的根据。如下面这段代码: /** * 基于字符串长度的比对 */ public class LengthComparator implements Comparator{ public int compare(Object obj1, Object obj2) { String str1=String.valueOf(obj1); String str2=String.valueOf(obj2); return str1.length()-str2.length(); } }

Page 64: Hibernate的高级操作

作为示例,LengthComparator 实现了字符串长度的比对,我们可以在配置中指定 LengthComparator

作为排序算法: <set name="addresses" lazy="true" table="t_address" sort="org.sample.LengthComparator"> <key column="user_id"/> <element type="string" column="address"/> </set>

Map 类型的排序与 Set 基本一致: <map name="addresses" lazy="true" table="t_address" sort="org.sample.LengthComparator"> <key column="user_id "/> <element type="string" column="address"/> </map>

可排序 Map 在 Hibernate 中对应的实现类为 net.sf.hibernate.collection.SortedMap,它实现了

java.util.SortedMap 接口。

net.sf.hibernate.collection.SortedMap 和 net.sf.hibernate.collection.SortedSet 的内部实现分别基于

java.util.TreeSet 和 java.util.TreeMap。

而 Bag 和 List 由于实现原理的不同(且 JDK 中也并不存在所谓的 TreeList),并不支持 sort 排序

方式。

order-by

SQL 中的 “order by”子句对于大部分读者而言想必早已熟稔于心。Collection 的 order-by 排序

方式,其实现原理也是借助 SQL 的 order by 子句。

同样,我们首先来看一个简单的示例: <set name="addresses" lazy="true" table="t_address" order-by="address desc"> <key column="user_id"/> <element type="string" column="address"/> </set>

在 order-by 属性中,我们指定了 SQL 排序子句。Hibernate 在自动生成 SQL 时,会根据此项配置,

自动在 SQL 中追加相应的 order by 子句。

运行以下代码 TUser user = (TUser) session.load(TUser.class, new Integer(1)); Iterator it = user.getAddresses().iterator(); while(it.hasNext()){ String addr = (String)it.next(); System.out.println(addr); }

并观察 Hibernate 生成的 SQL 语句: Hibernate: select tuser0_.id as id0_, tuser0_.name as name0_, tuser0_.age as age0_,

tuser0_.version as version0_ from t_user tuser0_ where tuser0_.id=? Hibernate: select addresses0_.user_id as user_id__, addresses0_.address as

address__ from t_address addresses0_ where addresses0_.user_id=? order by addresses0_.address asc

Page 65: Hibernate的高级操作

可以看到,Hibernate 在生成 SQL 的时候,已经追加了 order-by 子句“order by addresses0_.address

asc”。

注意:

order-by 特性在实现中借助了 JDK1.4 中的新增集合类 LinkedHashSet 以及

LinkedHashMap。因此,order-by 特性只支持在 1.4 版本以上的 JDK 中运行。

Set、Map、Bag 均支持 order-by 排序,有序集 List 例外。

5.2 Hibernate 回调与拦截机制