为了避免在某些情况下,关联关系所带来的无谓的性能开销。
所谓延迟加载,就是在需要数据的时候,才真正执行数据加载操作。
Hibernate2中的延迟加载实现主要针对:
1. 实体对象。
2. 集合(Collection)。
Hibernate3同时提供了属性的延迟加载功能。
1. 实体对象的延迟加载
通过load方法可以指定可以返回目标实体对象的代理。
通过class的lazy属性,可以打开实体对象的延迟加载功能。(映射文件)
(Hibernate2中,lazy默认为false;Hibernate3默认true)
非延迟加载的例子:
1 2 3 4 5 6 7 8 9 10 11 | < hibernate-mapping >
< class name = "...TUser"
table = "t_user"
dynamic-update = "false"
dynamic-insert = "false"
select-before-update = "false"
optimistic-lock = "version"
lazy = "false"
> ... < hibernate-mapping > |
1 2 | TUser user = (Tuser)session.load(TUser. class , new Integer( 1 )); ( 1 ) System.out.println(user.getName()); ( 2 ) |
当程序运行到(1)时,Hibernate已经从库表中取出了对应的记录,并构造了一个完整的TUser对象。
对以上映射配置修改:
lazy=”true”
看代码运行至(1)后的user对象状态(Eclipse Debug视图)
可以看到,此时的user对象与我们之前定义的实体类并不相同,其当前类型描述为TUser$EnhancerByCGLIB$$bede8986,且其属性均为null。
同时观察屏幕日志,此时并没有任何Hibernate SQL输出,也就意味着,当我们获得user对象引用的时候,Hibernate并没有执行数据库查询操作。
代码运行至(2),再次观察user对象状态
看到user对象的name属性仍然是null,但是观察屏幕输出,看到查询操作已经执行,同时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.target中是否存在目标对象。
如果存在,则调用目标对象的getName方法返回,如果目标对象为空,则发起数据库查询指令,读取记录、构建目标对象并将其设入CGLIB$CALLBACK_0.target。
这样,通过一个中间代理,实现了数据延迟加载功能,只有当客户程序真正调用实体类的取值方法时,Hibernate才会执行数据库查询操作。
2. 集合类型的延迟加载
Hibernate延迟加载机制中,关于集合的延迟加载特性意义最为重大,也是实际应用中相当重要的一个环节。
如果我们只想要获得user的年龄(age)属性,而不关心user的地址信息(地址是集合类型),那么自动加载address的特性就显得特别多余,并造成了极大的性能浪费。
将前面一对多关系中的lazy属性修改为true,即指定了关联对象采用延迟加载:
1 2 3 4 5 6 7 | < hibernate-mapping >
< class name = "" table = "" dynamic-update = ""
dynamic-insert = "" >
...
< set name = "addresses" table = "t_address"
lazy = "true" ...> ... |
尝试执行以下代码:
1 2 3 4 5 6 7 8 9 | 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的address属性。
Hibernate.initialize方法可以强制Hibernate立即加载关联对象集:
1 2 3 4 5 6 7 | Hibernate.initialize(user.getAddress()); session.close(); //通过Hibernate.initialize方法强制读取数据 //addresses对象即可脱离session进行操作 Set hset = user.getAddresses(); Taddress addr = (Taddress)hset.toArray()[ 0 ];
|
为了实现透明化的延迟加载机制,Hibernate进行了大量努力。其中包括JDK 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)声明Colleciotn型属性的原因(如private Set addresses; 而非private HashSet addresses)。
当调用session.save(user);时,Hibernate如何处理其关联的Addresses对象集?
假设TUser定义如下:
1 2 3 4 5 | public class TUser implements Serializable{
…
private Set addresses = new HashSet();
… } |
我们通过Set接口,声明了一个addresses属性,并创建了一个HashSet作为addresses的初始实例,以便创建TUser实例后,就可以为其添加关联的address对象:
1 2 3 4 5 | TUser user = new TUser(); TAddress addr = new TAddress(); addr.setAddress(“HongKong”); user.getAddresses().add(addr); session.save(user); |
通过Eclipse的Debug视图,可以看到session.save方法执行前后user对象发生的变化:
首先,由于Insert操作,Hibernate获得数据库产生的id值,并填充到user对象的id属性。
另一方面,Hibernate使用了自己的Collection实现”net.sf.hibernate.collection.Set”对user中的HashSet型addresses属性进行了替换,并用数据对其进行填充,保证新的addresses与原有的addresses包含同样的实体元素。
再来看下面的代码:
1 2 3 4 5 6 7 | TUser user = (TUser)session.load(TUser. class , new Integer( 1 )); Collection addSet = user.getAddresses();( 1 ) Iterator it = addSet.iterator();( 2 ) while (it.hasNext()){
TAddress addr = (TAddress)it.next();
System.out.println(addr.getAddresses()); } |
当代码执行到(1)处时,addresses数据集尚未读入,我们得到的addrSet对象实际上只是一个未包含任何数据的net.sf.hibernate.collection.Set实例。
代码运行至(2),真正的数据读取操作才开始执行。
观察一下net.sf.hibernate.collection.Set.iterator方法可以看到:
1 2 3 4 | 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 user_id=?)。
这里引出另一个性能关注点,即关联对象的缓存策略。
如果我们为某个集合类设定了缓存,如:
1 2 3 4 5 6 7 8 9 10 11 12 | < 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 = "…TAddress" /> </ set > |
注意这里的<cache usage=”read-only”/>只会使得Hibernate对数据索引进行缓存,也就是说,这里的配置实际上只是缓存了集合中的数据索引,并不包括这个集合中的各个实体元素。
执行下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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.getAddresses()); } System.out.println( "\n=== Second Query ===\n" ); TUser user2 = (TUser)session2.load(TUser. class , new Integer( 1 )); Collection addSet2 = user2.getAddress(); //第二次加载user.addresses Iterator it2 = addSet2.iterator(); while (it2.hasNext()){
TAddress addr = (TAddress)it2.next();
System.out.println(addr.getAddress()); } |
观察屏幕日志输出:
Hibernate: select tuser0_.id as id3_0_, tuser0_.name as name3_0_, tuser0_.age as age3_0_, tuser0_.group_id as group4_3_0_ from t_user3 tuser0_ where tuser0_.id=?
Hibernate: select addresses0_.user_id as user7_1_, addresses0_.id as id1_, addresses0_.id as id7_0_, addresses0_.address as address7_0_, addresses0_.zipcode as zipcode7_0_, addresses0_.tel as tel7_0_, addresses0_.type as type7_0_, addresses0_.idx as idx7_0_, addresses0_.user_id as user7_7_0_ from t_address addresses0_ where addresses0_.user_id=? order by addresses0_.zipcode asc
Hongkong
Hongkong
=== Second Query ===
Hibernate: select tuser0_.id as id3_0_, tuser0_.name as name3_0_, tuser0_.age as age3_0_, tuser0_.group_id as group4_3_0_ from t_user3 tuser0_ where tuser0_.id=?
Hibernate: select taddress0_.id as id7_0_, taddress0_.address as address7_0_, taddress0_.zipcode as zipcode7_0_, taddress0_.tel as tel7_0_, taddress0_.type as type7_0_, taddress0_.idx as idx7_0_, taddress0_.user_id as user7_7_0_ from t_address taddress0_ where taddress0_.id=?
Hibernate: select taddress0_.id as id7_0_, taddress0_.address as address7_0_, taddress0_.zipcode as zipcode7_0_, taddress0_.tel as tel7_0_, taddress0_.type as type7_0_, taddress0_.idx as idx7_0_, taddress0_.user_id as user7_7_0_ from t_address taddress0_ where taddress0_.id=?
Hongkong
Hongkong
看到,第二次获取关联的addresses集合的时候,执行了2次Select SQL。
正是由于<set…><cache usage=”read-only”/>…</set>的设定,第一次addresses集合被加载之后,数据索引已经被放入缓存。
第二次再加载addresses集合的时候,Hibernate在缓存中发现了这个数据索引,于是从索引里面取出当前所有的id(此时数据库中有3条符合的记录,所以共获得3个id),然后依次根据3个id在缓存中查找对应的实体对象,但是没有找到,于是发起了数据库查询,由Select SQL根据id从t_address表中读取记录。
由于缓存中数据索引的存在,似乎SQL执行的次数更多了,这导致第二次借助的数据查询比第一次性能开销更大。
导致这个问题出现的原因何在?
这是由于我们只为集合类型配置了缓存,这样Hibernate只会缓存数据索引,而不会将集合中的实体元素同时也纳入缓存。
我们必须为集合类型中的实体对象也指定缓存策略,如:
1 2 3 4 5 6 7 8 9 10 11 12 | < hibernate-mapping >
< class
name = "…TAddress"
table = "t_address"
dynamic-update = "false"
dynamic-insert = "false"
select-before-update = "false"
optimistic-lock = "version"
>
< cache usage = "read-write" />
… </ hibernate-mapping > |
此时,Hibernate才会对集合中的实体也进行缓存。
再次运行以上代码:
两次输出好像一样,哪里有问题(?)
上面讨论了net.sf.hibernate.collection.Set.iterate方法,同样,观察net.sf.hibernate.collection.Set.size/isEmpty方法或者其他hibernate.collection中的同类型方法实现,我们可以看到同样的处理方式。
通过自定义Collection类型实现数据延迟加载的原理也就在于此。
这样,通过自身的Collection实现,Hibernate就可以在Collection层从容的实现延迟加载特性。只有程序真正读取这个Collection的内容时,才激发底层数据库操作,这为系统的性能提供了更加灵活的调整手段。
3. 属性的延迟加载
假设t_user表中存在一个长文本类型的Resume字段,此字段中保存了用户的简历数据。长文本字段的读取相对而言会带来较大的性能开销,因此,我们决定为其设为延迟加载,只有真正需要处理简历信息的时候,才从库表中读取。
首先,修改映射配置文件,将Resume字段的lazy属性设置为true:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | < hibernate-mapping >
< class
name = "…TUser"
table = "t_user"
batch-size = "5"
>
…
< property
name = "resume"
type = "java.lang.String"
column = "resume"
lazy = "true" />
</ class > </ hibernate-mapping > |
与实体和集合类型的延迟加载不同,Hibernate3属性延迟加载机制在配置之外,还需要借助类增强器对二进制Class文件进行强化处理(buildtime bytecode instrumentation)。
在这里,我们通过Ant调用Hibernate类增强器对TUser.class文件进行强化处理。Ant脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | < 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 > |
使用这个脚本时需要注意各个路径的配置。本例中,此脚本位于Eclipse项目的根目录下,./bin为Eclipse的默认编译输出路径,./bin下存放了执行所需的jar文件(hibernate3.jar及Hibernate所需的类库)。
以上Ant脚本将对TUser.class文件进行强化,如果对其进行反编译,可以看到如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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 InterceptFieldCallback 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类的方法调用,从而为延迟加载机制提供实现的技术基础。
经过以上处理,运行以下测试代码:
1 2 3 4 5 6 7 8 9 | String hql = “from TUser user where user.name=’Erica’”; Query query = session.createQuery(hql); List list = query.list(); Iterator it = list.iterator(); while (it.hasNext()){
TUser user = (TUser)it.next();
System.out.println(user.getName());
System.out.println(user.getResume()); } |
观察输出日志:
可以看到,在此过程中,Hibernate先后执行了两条SQL,第一句用于读取TUser类中的非延迟加载字段。而之后,当user.getResume()方法调用时,随即调用第二条SQL从库表中读取Resume字段数据。属性的延迟加载已经实现。