Hibernate高级特性



Hibernate高级特性。其内部实现方式对应用层可能产生怎样的影响,则是我们下面需要关注的问题.
本章主要分两大部分进行介绍:
1. Hibernate持久化实现

介绍Hibernate对象持久化操作的实现机理.其中包括以下内容:
实体对象生命周期
实体对象识别
数据缓存
事务管理

持久层操作
2. Hibernate回调与拦截机制
介绍了Hibernate中提供的事件捕获和处理机制.其中包括以下内容:
Lifecyle与Validatable接口
Hibernate Interceptor
5.1 Hibernate持久化实现

5.1.1 实体对象生命周期
实体对象的3种状态

实体对象的生命周期,是Hibernate应用中的一个关键概念.对生命周期的理解和把握,不仅对
Hibernate的正确应用颇有裨益,而且对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的有效期内,那
么它就处于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状态.

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. 如果配备了Ve r s i o n属性(参见稍后的”事务管理-乐观锁”部分描述),version属性是否
为null.
4.
如果配备了Ve r s i o n属性,且为vesion指定了unsaved-value,version属性值是否等于
unsave-value.

5.
如果存在Interceptor(参见稍后的”Hibernate回调与拦截机制”部分内容),那么
Interceptor.isUnsaved方法是否返回true.

相对与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操作.

下面的例子中,我们把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语言规范的角度而言,这两个对象无论是引用,还是具体内容都不相同.但是,站在持
久层角度而言,这两个对象却都代表着数据库中的同一条记录(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());

观察屏幕输出: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));

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
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/)

另外注意,使用”值比对”方法只需针对实体类的属性进行处理,而不要涉及实体类所关联的集
合类的比对,否则在多对多关系中很容易引发一些其他的问题.

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

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

业务关键信息判定实际上是值比对的一个子集,也就是说,在进行实体属性比对的时候,我们只
对一些业务关键属性进行判断,如之前讨论业务逻辑层的对象判定时所提及的例子:如果两个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模式的描述.

常这也就意味着数据对象的内容发生变化),则将其标志为”待更新”状态,之后在数据库
操作时将其更新到对应的库表.
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; //版本号[用于乐观锁,请参见事务管理部分]
//
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实例相关联的实体对象的当前实例和原始状态信息(即
EntityEntry).这两者以”key-value”的形式,保存在SessionImpl.entityEntries数据结构中.

SessionImpl.entityEntries是一个Map型的数据结构,其中每个项目(Entry)都包含了当前与Session
关联的一个实体对象实例及其原始信息.以实体对象为Key,而以对应的EntityEntry为Va
l u e.

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 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);
图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,
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)的系统而言(如论坛系统,新闻发布系统等),良好的缓存管理机制以及合理的缓存
应用模式往往是性能提升的关键.

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

持久层设计中,往往需要考虑到几个不同层次中的数据缓存策略.这些层次的划分标准针对不同
的情况有所差异,一般而言,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式缓存而言),这样的远程同步开销不可忽视.

笔者曾经主持构建的一个大型金融业务系统中,在模拟测试阶段发现,分布式缓存的频繁同步甚
至导致了网络中的数据阻塞.

考虑到主流企业级数据库均已经具备了数据库级的缓存机制,此时,分布式缓存的性能优势在一
些情况下并不明显,并且还可能引入其他的问题,因此,分布式缓存的使用还有待商榷.当我们决定
在系统中引入分布式缓存前,必须经过仔细的压力测试和性能分析,以免出现不必要的尴尬.

另外,需要再次强调的是,如果当前应用与其他应用共享数据库,也就是说,在当前应用运行过
程中,其他应用可能同时更新数据库,那么缓存策略的制定就需要格外小心.这种情况下,采取一些
保守策略(避免缓存机制的使用)可能更加稳妥.

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
将某个特定对象从内部缓存中清除.
2. Session.clear
清空内部缓存.
二级缓存

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

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

Session在进行数据查询操作时,会首先在自身内部的一级缓存中进行查找,如果一级缓存未能
命中,则将在二级缓存中查询,如果二级缓存命中,则以此数据作为结果返回.

在引入二级缓存时,我们首先必须考虑以下问题:
1. 数据库是否与其他应用共享
2. 应用是否需要部署在集群环境中

对于第一种情况,往往也就意味着我们不得不放弃二级缓存的使用(我们也可以对数据库的共享
情况进行细化,比如某个表由本应用独占,那么也可以对此表引用二级缓存机制).

对于第二种情况,我们就必须考虑是否需要引入分布式缓存机制,以及引入分布式缓存带来的实
际性能变化.
其次,我们应该对哪些数据应用二级缓存

显然,对数据库中所有的数据都实施缓存是最简单的方法,大多数情况下,这可能也是实际开发
中最常采用的模式(节省了开发人员的大量脑细胞 ).

但是在某些情况下,这样的方式反而会对性能造成影响,如对于以下情况:一个电信话务系统,
客户可以通过这套系统查询自己的历史通话记录.

这个案例中,对于每个客户,库表中可能都有成千上万条数据,而不同客户之间,基本不可能共
享数据(客户只能查询自身的通话记录),如果对此表施以缓存管理,那么可以想象,内存会迅速被
几乎不可能再被重用的数据充斥,系统性能急剧下降.

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

如果数据满足以下条件,则可将其纳入缓存管理.
1. 数据不会被第三方应用修改
2. 数据大小(Data Size)在可接受的范围之内

3. 数据更新频率较低
4. 同一数据可能会被系统频繁引用
5. 非关键数据(关键数据,如金融账户数据)

Hibernate本身并未提供二级缓存的产品化实现(只是提供了一个基于Hashtable的简单缓存以供
调试),而是为众多的第三方缓存组件提供了接入接口,我们可以根据实际情况选择不同的缓存实现
版本,具体请参见稍后的”第三方缓存实现”部分内容的描述.

第三方缓存实现

基于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为例):

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

net.sf.ehcache.hibernate.Provider

……

另外还需要针对Cache实现本身进行配置,下面是一个EHCache配置文件示例:ehcache.xml:

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

之后,需要在我们的映射文件中指定各个映射实体(以及collection)的缓存同步策略:

….

….

缓存同步策略可应用于实体类和集合属性.
下面,我们继续围绕缓存同步策略进行探讨.

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

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

Hibernate提供以下4种内置的缓存同步策略:
read-only
只读.对于不会发生改变的数据,可使用只读型缓存.

nonstrict-read-write

如果程序对并发访问下的数据同步要求不是非常严格,且数据更新操作频率较低(几个
小时或者更长时间更新一次),可以采用本选项,获得较好的性能.

read-write
严格可读写缓存.基于时间戳判定机制,实现了”read
committed”事务隔离等级3.可
用于对数据同步要求严格的情况,但不支持分布式缓存.这也是实际应用中使用最多的同步
策略.

3
参见稍后”事务管理”部分中对于事务隔离等级的描述.
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(隔离性)

事务允许多个用户对同一个数据的并发访问,而不破坏数据的正确性和完整性.同时,并行
事务的修改必须与其他并行事务的修改相互独立.

按照比较严格的隔离逻辑来讲,一个事务看到的数据要么是另外一个事务修改这些事务之前
的状态,要么是第二个事务已经修改完成的数据,但是这个事务不能看到其他事务正在修改的数
据.

针对不同的情况,事务的隔离级别要求也各有差异,下面一节中,我们将具体探讨事务隔离
等级的相关内容.
4. Durability(持久性)

事务结束后,事务处理的结果必须能够得到固化(保存在可掉电存储器上).
数据库事务管理隔离等级

事务隔离指的是,数据库(或其他事务系统)通过某种机制,在并行的多个事务之间进行分隔,
使每个事务在其执行过程中保持独立(如同当前只有此事务单独运行).

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

Hibernate中的事务隔离依赖于底层数据库提供的事务隔离机制,因此,对数据库事务隔离机制的
理解在基于Hibernate实现的持久层中同样适用.

首先我们来看数据操作过程中可能出现的3种不确定情况:
脏读取(Dirty Reads)
一个事务读取了另一个并行事务未提交的数据.

不可重复读取(Non-repeatable Reads)
一个事务再次读取之前曾读取过的数据时,发现该数据已经被另一个已提交的事务修
改.

虚读(Phantom Reads)

一个事务重新执行一个查询,返回一套符合查询条件的记录,但这些记录中包含了因
为其他最近提交的事务而产生的新记录.

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

最低等级的事务隔离,它仅仅保证了读取过程中不会读取到非法数据.这种隔离等级
下,上述3种不确定情况均有可能发生.

此事务等级对于大多数逻辑严格的应用系统而言是难以接受的,脏读取的出现将为系
统的并发逻辑带来极大的隐患.
Read Committed

此级别的事务隔离保证了一个事务不会读到另一个并行事务已修改但未提交的数据,
也就是说,此等级的事务级别避免了”脏读取”.

当一个事务运行在这个隔离级别时, 一个 SELECT
查询只能看到查询开始之前提交
的数据,而永远无法看到未提交的数据,或者是在查询执行时其他并行的事务提交做的改
变.

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

此级别的事务隔离避免了”脏读取”和”不可重复读取”现象的出现.这也意味着,一
个事务不可能更新已经由另一个事务读取但未提交(回滚)的数据.

一般而言,此级事务应用并不广泛,它并不能完全保证数据的合法性(可能出现虚读),
同时也带来了更多的性能损失,如果当前数据库由应用所独享,那么我们可以考虑通过”乐
观锁”达到同样的目的(参见稍后关于”锁”机制的探讨).

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作
为事务管理实现:

……

net.sf.hibernate.transaction.JTATransactionFactory

… …

基于JDBC的事务管理

将事务管理委托给JDBC进行处理无疑是最简单的实现方式,Hibernate对于JDBC事务的封装也
非常简单.
我们来看下面这段代码:

session = sessionFactory.openSession();
Transaction tx =
session.beginTransaction();
……
tx.commit();
从JDBC层面而言,上面的代码实际上对应着:

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
Transaction
Start
Transaction
Commit
JTA
Container
JDBC Connection 1JDBC Connection 1JDBC Connection
1……
Transaction Commit
Transaction
Start
JDBC Tran saction
JTA
Tran saction
图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{
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)”.
悲观锁(Pessimistic Locking)

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的
事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态.悲观锁的实现,
往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,
即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据).

一个典型的,依赖数据库实现的悲观锁调用:
select * from account where ” 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之前)设定加锁,才会真正通过数据库
的锁机制进行加锁处理,否则,数据已经通过不包含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提供的透明化乐观锁实现,将大大提升我们的生产力.

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

现在,我们为之前示例中的TUser加上乐观锁机制.
1. 首先为TUser的class描述符添加optimistic-lock属性:

……

optimistic-lock属性有如下可选取值:
none
无乐观锁.

version
通过版本机制实现乐观锁.
dirty
通过检查发生变动过的属性实现乐观锁.
all

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

其中通过version实现的乐观锁机制是Hibernate官方推荐的乐观锁实现,同时也是Hibernate中,
目前惟一在实体对象脱离Session发生修改的情况下依然有效的锁机制.因此,一般情况下,我们都
选择version方式作为Hibernate乐观锁实现机制.

2. 添加一个Version属性描述符

……

注意,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

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

查询性能往往是系统性能表现的一个重要方面.相对数据库更新,删除操作而言,查询机制的优
劣很大程度上决定了系统的整体性能.

同样,这个领域,往往也存在最大的性能调整空间.对于同样的查询结果,不同的实现机制其性
能差距可能超出大多数人的想象(出现几百倍的性能差距并不奇怪).

因此,在开始具体应用开发之前,了解这方面的实现原理和机制是非常必要的,而这,也是我们
下面即将讨论的主题.
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 )
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());
}

得到屏幕输出:
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 )
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方法执行所引发的操作日志.

这里,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;iTUser 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 )
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方法则可以充分发挥缓存带
来的优势,如果目标数据只读或者读取相对较为频繁,通过这种机制可以大大减少性能上的损耗.

这是基于充分利用缓存以提升性能上的考量.
同时,另外一方面,还有内存使用上的考虑.

假设我们需要对海量数据进行操作,那么,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,从缓存
中取出对应的实体对象返回.

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选项:

……
true
……

之后我们必须在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;iTUser 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 )
Sammi
Luna
Second Query…

Sammi
Luna
看到,第二次查询期间,Hibernate并没有执行任何Select SQL即完成了任务,这就是Query
Cache的
作用.
延迟加载(Lazy Loading)

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

如,之前示例中TUser对象在加载的时候,在非”延迟加载”的情况下,会同时读取其所关联的
多个地址(address)对象,对于确实需要对address进行操作的应用逻辑而言,关联数据的自动加载机
制的确非常有效.

但是,如果我们只是想要获得user的年龄(age)属性,而不关心user的地址(address)信息,那
么自动加载address的特性就显得多余,并且造成了极大的性能浪费.为了获得user的性别属性,我们
可能还要同时从数据库中读取数条无用的地址数据,这导致了大量无谓的系统开销.

延迟加载特性的出现,正是为了解决这个问题.
所谓延迟加载,就是在需要数据的时候,才真正执行数据加载操作.
Hibernate
2中的延迟加载实现主要针对:
1. 实体对象.
2. 集合(Collection).
Hibernate 3
同时提供了属性的延迟加载功能.
实体对象的延迟加载

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

而这个代理,在这个过程中起着怎样的角色 这正是下面我们需要探讨的话题.
首先我们来看正常情况下,一个非延迟加载的例子:

……

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.
图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对象.

那么,使用了延迟加载机制之后,情况会是怎样
为了使用实体的延迟加载功能,我们对以上映射配置修改如下:

……

通过class的lazy属性,我们可以打开实体对象的延迟加载功能.
那么,延迟加载具有怎样的特性 尝试在Eclipse
Debug视图中观察以下代码的运行情况:
TUser user = (TUser)session.load(TUser.class,new
Integer(1)); ⑴
System.out.println(user.getName()); ⑵

首先来看代码运行至⑴后的user对象状态(图5-7):
图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属性也正确输出.
两次查询操作为什么会有这样的差异
原因就在于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,即指定了关联对象采用延迟加载:

……

……

尝试执行以下代码:
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 “+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实现.
这也正是我们为什么在编写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));

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= ).

这里引出了另外一个性能关注点,即关联对象的缓存策略.
如果我们为某个集合类设定了缓存,如:

注意这里的只会使得Hibernate对数据索引进行缓存,也就是说,这
里的配置实际上只是缓存了集合中的数据索引,而并不包括这个集合中的各个实体元素.

执行下面的代码:
TUser user = (TUser)session.load(TUser.class,new Integer(1));

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.

正是由于…的设定,第一次addresses集合被加载之后,
数据索引已经被放入缓存.

第二次再加载addresses集合的时候,Hibernate在缓存中发现了这个数据索引,于是从索引里面
取出当前所有的id(此时数据库中有3条符合的记录,所以共获得3个id),然后依次根据这3个id
在缓存中查找对应的实体对象,但是没有找到,于是发起了数据库查询,由Select
SQL根据id从
t_address表中读取记录.

我们看到,由于缓存中数据索引的存在,似乎SQL执行的次数更多了,这导致第二次借助缓存
的数据查询比第一次性能开销更大.

导致这个问题出现的原因何在

这是由于我们只为集合类型配置了缓存,这样Hibernate只会缓存数据索引,而不会将集合中的
实体元素同时也纳入缓存.

我们必须为集合类型中的实体对象也指定缓存策略,如:

……

此时,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字段,此字段中保存了用户的简历数据.长文
本字段的读取相对而言会带来较大的性能开销,因此,我们决定将其设为延迟加载,只有真正需要处
理简历信息时,才从库表中读取.

首先,修改映射配置文件,将Resume字段的lazy属性设置为true:

与实体和集合类型的延迟加载不同.Hibernate
3属性延迟加载机制在配置之外,还需要借助类增
强器对二进制Class文件进行强化处理(buildtime bytecode
instrumentation).
在这里,我们通过Ant调用Hibernate类增强器对TUser.class文件进行强化处理.Ant脚本如下:

使用这段脚本时需要注意各个路径的配置.本例中,此脚本位于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’)
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状态
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对象处于怎样
一个状态.

下面是可能出现的两种情况:
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);
}

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的数量:

……

25
……

这样,当我们发起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 “+(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次,取平均数)

可能大家对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_
Hibernate:
delete from T_User where >Hibernate: delete from T_User where >Hibernate:
delete from T_User where >……

Hibernate会首先从数据库查询出所有符合条件的记录,再对此记录进行循环删除,实际上,
session.delete(“from
TUser”)等价于:
Transaction tx = session.beginTransaction();

List
userList = session.find(“from TUser”);
int len = userList.size();

for (int i=0;isession.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; i5″完成所有的工作呢

这也就是所有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操作.

另外,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();
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 ;
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
User name is ==> ERICA

可以看到,第二次加载操作成功,由于缓存同步上的问题,我们得到了一个已被删除的过期数据
对象.

通过前面的讨论,我们知道,Hibernate中维护了两级缓存:
1. 内部缓存(Session Level Cache)

在Session生命周期内存在,每个Session中都维护了一个独立缓存,为当前Session实例所独享.
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 ;
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
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根据这些接口提供了自己的实现.
我们这里所说的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不同).这个实
例对应的映射配置如下:

……

……

但如果我们并不是通过实体对象(TAddress)进行映射,而是直接通过String对t_address表的
address字段进行组合映射,就会触发一个陷阱.

如在TUser.hbm.xml中进行如下修改:
……

……

上面的配置,直接将t_address表的address字段与TUser相关联.TUser对象的addresses集合由
来自t_address表对应记录的address字段(String类型)填充.

再回忆一下Java语言基础中关于字符串比较的内容.
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”声明的
数据集合,其元素排列顺序将不会被持久化.

为了说明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就可
以准确定位库表记录,从而实现高效的数据操作.

类似类/表映射关系,这里我们也可以指定id生成机制.不过,在目前的版本中,尚不支持native
类型的id生成机制.

3. Map
Map同样是一个无序集合类型,与Set/Bag不同的是,Map提供了键值对应关系.
一个典型的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”));
打印输出如下:
Home Address is: Shanghai

Office Address is: Beijing
示例库表数据如图5-11所示.
图5-11 示例中的库表数据
4.
List

与前面几种不同,List实现了集合内元素顺序的持久化.与Map集合需要额外字段保存键值一样,
它要求库表必须配属对应的字段以保持次序信息.

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 执行后的库表数据
5.1.8 结果集排序

之前关于Collection部分的讨论中,我们引入了所谓有序集和无序集的概念.无序集和有序集,
是针对Hibernate数据持久过程中,是否保持数据集合中的记录排列顺序加以区分的.

也就是说,对于一个有序集,其中元素的排列次序将会在库表中制定的字段中保存,而下次读取
时,也会以同样的次序排列.

而下面我们所要探讨的,则是关于Collection中的元素排序问题.

排序强调的是针对现有数据,以特定的逻辑对其排列次序进行调整.而排序的结果,是数据在内
存中的某种排列次序,属于临时状态.

数据排序有两种方式:
1. Sort
Collection中的数据排序.如对一个List中的元素先后顺序进行调整.
2.
order-by
对数据库执行Select SQL时,由order by子句实现的数据排序方式.

可以看出,这两种排序方式的最基本差异在于,Sort操作是在JVM中完成.而order-by是由数
据库完成.

下面我们分别就这两种类型的排序方式进行探讨:
Sort
首先来看一个简单的示例:

可排序Set在Hibernate中对应的实现类为net.sf.hibernate.collection.SortedSet,它实现了
java.util.SortedSet接口.

sort=”natural”指定采用Java默认排序机制,它会调用相应数据类型的compareTo方法进行排序中
的值比对.这里指定了元素类型为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();
}
}

作为示例,LengthComparator实现了字符串长度的比对,我们可以在配置中指定LengthComparator
作为排序算法:

Map类型的排序与Set基本一致:

可排序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子句.

同样,我们首先来看一个简单的示例:

在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
可以看到,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回调与拦截机制