`

Hibernate延迟加载续

阅读更多

本文主要就Hibernate抓取策略加以分析,Hibernate抓取策略(fetching strategy)是指:当应用程序需要在(Hibernate实体对象图的)关联关系间进行导航的时候, Hibernate如何获取关联对象的策略。

抓取策略可以在O/R映射的元数据中声明,也可以在特定的HQL 或条件查询(Criteria Query)中重载声明。

如下几种Hibernate抓取策略

◆连接抓取(Join fetching) - Hibernate通过 在SELECT语句使用OUTER JOIN(外连接)来 获得对象的关联实例或者关联集合。

◆查询抓取(Select fetching) - 另外发送一条 SELECT 语句抓取当前对象的关联实体或集合。除非你显式的指定lazy="false"禁止 延迟抓取(lazy fetching),否则只有当你真正访问关联关系的时候,才会执行第二条select语句。

◆子查询抓取(Subselect fetching) - 另外发送一条SELECT 语句抓取在前面查询到(或者抓取到)的所有实体对象的关联集合。除非你显式的指定lazy="false" 禁止延迟抓取(lazy fetching),否则只有当你真正访问关联关系的时候,才会执行第二条select语句。

◆批量抓取(Batch fetching) - 对查询抓取的优化方案, 通过指定一个主键或外键列表,Hibernate使用单条SELECT语句获取一批对象实例或集合。

Hibernate抓取策略会区分下列各种情况:

1.Immediate fetching,立即抓取 - 当宿主被加载时,关联、集合或属性被立即抓取。

2.Lazy collection fetching,延迟集合抓取- 直到应用程序对集合进行了一次操作时,集合才被抓取。(对集合而言这是默认行为。)

3."Extra-lazy" collection fetching,"Extra-lazy"集合抓取 -对集合类中的每个元素而言,都是直到需要时才去访问数据库。除非绝对必要,Hibernate不会试图去把整个集合都抓取到内存里来(适用于非常大的集合)。

4.Proxy fetching,代理抓取 - 对返回单值的关联而言,当其某个方法被调用,而非对其关键字进行get操作时才抓取。

5."No-proxy" fetching,非代理抓取 - 对返回单值的关联而言,当实例变量被访问的时候进行抓取。与上面的代理抓取相比,这种方法没有那么“延迟”得厉害(就算只访问标识符,也会导致关联抓取)但是更加透明,因为对应用程序来说,不再看到proxy。这种方法需要在编译期间进行字节码增强操作,因此很少需要用到。

6.Lazy attribute fetching,属性延迟加载 - 对属性或返回单值的关联而言,当其实例变量被访问的时候进行抓取。需要编译期字节码强化,因此这一方法很少是必要的。

这里有两个正交的概念:关联何时被抓取,以及被如何抓取(会采用什么样的SQL语句)。不要混淆它们!我们使用抓取来改善性能。我们使用延迟来定义一些契约,对某特定类的某个脱管的实例,知道有哪些数据是可以使用的。

1.操作延迟加载的关联

默认情况下,Hibernate 3对集合使用延迟select抓取,对返回单值的关联使用延迟代理抓取。对几乎是所有的应用而言,其绝大多数的关联,这种策略都是有效的。

注意:假若你设置了hibernate.default_batch_fetch_size,Hibernate会对延迟加载采取批量抓取优化措施(这种优化也可能会在更细化的级别打开)。

然而,你必须了解延迟抓取带来的一个问题。在一个打开的Hibernate session上下文之外调用延迟集合会导致一次意外。比如:

  1. s = sessions.openSession();  
  2. Transaction tx = s.beginTransaction();  
  3.               
  4. User u = (User) s.createQuery("from User u where u.name=:userName")  
  5.     .setString("userName", userName).uniqueResult();  
  6. Map permissions = u.getPermissions();  
  7.  
  8. tx.commit();  
  9. s.close();  
  10.  
  11. Integer accessLevel = (Integer) permissions.get("accounts");  // Error! 

在Session关闭后,permessions集合将是未实例化的、不再可用,因此无法正常载入其状态。 Hibernate对脱管对象不支持延迟实例化. 这里的修改方法是:将permissions读取数据的代码 移到tx.commit()之前。

除此之外,通过对关联映射指定lazy="false",我们也可以使用非延迟的集合或关联。但是, 对绝大部分集合来说,更推荐使用延迟方式抓取数据。如果在你的对象模型中定义了太多的非延迟关联,Hibernate最终几乎需要在每个事务中载入整个数据库到内存中!

但是,另一方面,在一些特殊的事务中,我们也经常需要使用到连接抓取(它本身上就是非延迟的),以代替查询抓取。 下面我们将会很快明白如何具体的定制Hibernate中的抓取策略。在Hibernate3中,具体选择哪种抓取策略的机制是和选择 单值关联或集合关联相一致的。

2.  调整抓取策略(Tuning fetch strategies)

查询抓取(默认的)在N+1查询的情况下是极其脆弱的,因此我们可能会要求在映射文档中定义使用连接抓取:

  1. <set name="permissions"   
  2.             fetch="join"> 
  3.     <key column="userId"/> 
  4.     <one-to-many class="Permission"/> 
  5. set 
  6. <many-to-one name="mother" class="Cat" fetch="join"/> 

在映射文档中定义的抓取策略将会对以下列表条目产生影响:通过get()或load()方法取得数据。只有在关联之间进行导航时,才会隐式的取得数据。

条件查询,使用了subselect抓取的HQL查询

不管你使用哪种抓取策略,定义为非延迟的类图会被保证一定装载入内存。注意这可能意味着在一条HQL查询后紧跟着一系列的查询。

通常情况下,我们并不使用映射文档进行抓取策略的定制。更多的是,保持其默认值,然后在特定的事务中, 使用HQL的左连接抓取(left join fetch) 对其进行重载。这将通知 Hibernate在第一次查询中使用外部关联(outer join),直接得到其关联数据。 在条件查询 API中,应该调用 setFetchMode(FetchMode.JOIN)语句。

也许你喜欢仅仅通过条件查询,就可以改变get() 或 load()语句中的数据抓取策略。例如:

  1. User user = (User) session.createCriteria(User.class)  
  2.                 .setFetchMode("permissions", FetchMode.JOIN)  
  3.                 .add( Restrictions.idEq(userId) )  
  4.                 .uniqueResult(); 


(这就是其他ORM解决方案的“抓取计划(fetch plan)”在Hibernate中的等价物。)截然不同的一种避免N+1次查询的方法是,使用二级缓存。

 

 

 

 

 

 

学习Hibernate时,经常会遇到实体对象延迟加载问题,这里将介绍问题的解决方法Hibernate实现实体对象延迟加载。

延迟加载机制是为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是当在真正需要数据的时候,才真正执行数据加载操作。在Hibernate中提供了对实体对象的延迟加载以及对集合的延迟加载,另外在Hibernate3中还提供了对属性的延迟加载。下面我们就分别介绍这些种类的延迟加载的细节。

Hibernate实现实体对象延迟加载:

如果想Hibernate实现实体对象延迟加载,必须要在实体的映射配置文件中进行相应的配置,如下所示:


  1. <hibernate-mapping>

  2. <classname=”com.neusoft.entity.User” table=”user” lazy=”true”>

  3. ……  

  4. </class>

  5. </hibernate-mapping>

通过将class的lazy属性设置为true,来开启实体的延迟加载特性。如果我们运行下面的代码:


  1. User user=(User)session.load(User.class,”1”);  

  2. System.out.println(user.getName());

当运行到第一条时,Hibernate并没有发起对数据的查询,如果我们此时通过一些调试工具(比如JBuilder2005的Debug工具),观察此时user对象的内存快照,我们会惊奇的发现,此时返回的可能是 User$EnhancerByCGLIB$$bede8986类型的对象,而且其属性为null,这是怎么回事?还记得前面我曾讲过session.load()方法,会返回实体对象的代理类对象,这里所返回的对象类型就是User对象的代理类对象。在Hibernate中通过使用CGLIB,来实现动态构造一个目标对象的代理类对象,并且在代理类对象中包含目标对象的所有属性和方法,而且所有属性均被赋值为null。通过调试器显示的内存快照,我们可以看出此时真正的User对象,是包含在代理对象的CGLIB$CALBACK_0.target属性中,当代码运行到第二条时,此时调用user.getName()方法,这时通过CGLIB赋予的回调机制,实际上调用 CGLIB$CALBACK_0.getName()方法,当调用该方法时,Hibernate会首先检查CGLIB$CALBACK_0.target属性是否为null,如果不为空,则调用目标对象的getName方法,如果为空,则会发起数据库查询,生成类似这样的SQL语句:select * from user where id=’1’;来查询数据,并构造目标对象,并且将它赋值到 CGLIB$CALBACK_0.target属性中。

这样,通过一个中间代理对象,Hibernate实现实体对象延迟加载,只有当用户真正发起获得实体对象属性的动作时,才真正会发起数据库查询操作。所以实体的延迟加载是用通过中间代理类完成的,所以只有session.load()方法才会利用实体延迟加载,因为只有session.load()方法才会返回实体类的代理类对象。

 

为了避免在某些情况下,关联关系所带来的无谓的性能开销。

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

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>
<classname="...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, newInteger(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>
<classname=""table=""dynamic-update=""
dynamic-insert="">
...
<setname="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];

System.out.println(addr.getAddress());

 

 

为了实现透明化的延迟加载机制,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
publicclassTUser implementsSerializable{
privateSet addresses = newHashSet();
}

我们通过Set接口,声明了一个addresses属性,并创建了一个HashSet作为addresses的初始实例,以便创建TUser实例后,就可以为其添加关联的address对象:

1
2
3
4
5
TUser user = newTUser();
TAddress addr = newTAddress();
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, newInteger(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
publicIterator iterator(){
read();
returnnewIteratorProxy(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"
>
<cacheusage="read-only"/>
<keycolumn="user_id"/>
<one-to-manyclass="…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, newInteger(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, newInteger(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"
>
<cacheusage="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
<projectname="HibernateSample"default="instrument” basedir=".">
<propertyname="lib.dir"value="./lib"/>
<propertyname="classes.dir"value="./bin"/>
<pathid="lib.class.path">
<filesetdir="${lib.dir}">
<includename="**/*.jar"/>
</fileset>
</path>
<targetname="instrument">
<taskdefname="instrument"
classname="org.hibernate.tool.instrument.InstrumentTask">
<classpathpath="${classes.dir}"/>
<classpathrefid="lib.class.path"/>
</taskdef>
<instrumentverbose="true">
<filesetdir="${classes.dir}/com/redsaga/hibernate/db/entity">
<includename="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
packagecom.redsaga.hibernate.db.entity;
importjava.io.Serializable;
importjava.util.Set;
importnet.sf.cglib.transform.impl.InterceptFieldCallback;
importnet.sf.cglib.transform.impl.InterceptFieldEnabled;
publicclassTUser
implementsSerializable, InterceptFieldEnabled {
publicInterceptFieldCallback getInterceptFieldCallback(){
return$CGLIB_READ_WRITE_CALLBACK;
}
publicInterceptFieldCallback setInterceptFieldCallback(
InterceptFieldCallback interceptFieldcallback){
$CGLIB_READ_WRITE_CALLBACK= interceptFieldcallback;
}
…略…
publicString $cglib_read_resume(){
resume;
if($CGLIB_READ_WRITE_CALLBACK!=null) goto_L2;elsegoto_L1;
_L1:return;
_L2:String s;
s;
return(String) $CGLIB_READ_WRITE_CALLBACK.readObject(this,”resume”,s);
}
publicvoid$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字段数据。属性的延迟加载已经实现。

为了避免在某些情况下,关联关系所带来的无谓的性能开销。

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

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>
<classname="...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, newInteger(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>
<classname=""table=""dynamic-update=""
dynamic-insert="">
...
<setname="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];

System.out.println(addr.getAddress());

 

 

为了实现透明化的延迟加载机制,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
publicclassTUser implementsSerializable{
privateSet addresses = newHashSet();
}

我们通过Set接口,声明了一个addresses属性,并创建了一个HashSet作为addresses的初始实例,以便创建TUser实例后,就可以为其添加关联的address对象:

1
2
3
4
5
TUser user = newTUser();
TAddress addr = newTAddress();
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, newInteger(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
publicIterator iterator(){
read();
returnnewIteratorProxy(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"
>
<cacheusage="read-only"/>
<keycolumn="user_id"/>
<one-to-manyclass="…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, newInteger(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, newInteger(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"
>
<cacheusage="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
<projectname="HibernateSample"default="instrument” basedir=".">
<propertyname="lib.dir"value="./lib"/>
<propertyname="classes.dir"value="./bin"/>
<pathid="lib.class.path">
<filesetdir="${lib.dir}">
<includename="**/*.jar"/>
</fileset>
</path>
<targetname="instrument">
<taskdefname="instrument"
classname="org.hibernate.tool.instrument.InstrumentTask">
<classpathpath="${classes.dir}"/>
<classpathrefid="lib.class.path"/>
</taskdef>
<instrumentverbose="true">
<filesetdir="${classes.dir}/com/redsaga/hibernate/db/entity">
<includename="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
packagecom.redsaga.hibernate.db.entity;
importjava.io.Serializable;
importjava.util.Set;
importnet.sf.cglib.transform.impl.InterceptFieldCallback;
importnet.sf.cglib.transform.impl.InterceptFieldEnabled;
publicclassTUser
implementsSerializable, InterceptFieldEnabled {
publicInterceptFieldCallback getInterceptFieldCallback(){
return$CGLIB_READ_WRITE_CALLBACK;
}
publicInterceptFieldCallback setInterceptFieldCallback(
InterceptFieldCallback interceptFieldcallback){
$CGLIB_READ_WRITE_CALLBACK= interceptFieldcallback;
}
…略…
publicString $cglib_read_resume(){
resume;
if($CGLIB_READ_WRITE_CALLBACK!=null) goto_L2;elsegoto_L1;
_L1:return;
_L2:String s;
s;
return(String) $CGLIB_READ_WRITE_CALLBACK.readObject(this,”resume”,s);
}
publicvoid$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字段数据。属性的延迟加载已经实现。

分享到:
评论

相关推荐

    Hibernate延迟加载以及利用Spring

    ### Hibernate延迟加载以及利用Spring #### 一、Hibernate延迟加载概念与原理 在理解Hibernate的延迟加载机制之前,我们首先需要了解什么是延迟加载。延迟加载(Lazy Loading)是一种设计模式,其核心思想是在真正...

    hibernate 延迟加载深入剖析

    ### Hibernate延迟加载深入剖析 #### 一、概述 在现代软件开发中,特别是与数据库交互的应用场景下,Hibernate作为一款流行的Java持久层框架,提供了多种高效处理数据的技术。其中,延迟加载(Lazy Loading)是一...

    什么是hibernate延迟加载

    详细介绍hibernate延迟加载,对hibernate初学者有一定的帮助

    Hibernate 延迟加载剖析与代理模式应用

    《Hibernate延迟加载与代理模式解析》 在Java的持久化框架Hibernate中,延迟加载(Lazy Load)是一项重要的优化策略,其核心目标是提高系统性能,减少内存占用,避免不必要的数据库交互。延迟加载允许我们在需要...

    hibernate延迟加载解决

    ### Hibernate延迟加载详解 #### 一、什么是延迟加载? 延迟加载是一种优化技术,在软件开发中广泛应用于各种场景,尤其在数据库交互中尤为重要。其核心思想是仅在确实需要某个资源时才加载它,而非一开始就加载...

    Hibernate延迟加载机制.zip

    Hibernate延迟加载机制.zip

    hibernate 延迟加载.docx

    【hibernate 延迟加载】 在Java的持久化框架Hibernate中,延迟加载(Lazy Loading)是一种优化数据库访问性能的技术。它允许我们在需要时才加载关联的对象,而不是在初始查询时就一次性加载所有数据。这有助于减少...

    Hibernate延迟加载介绍.doc

    《Hibernate延迟加载详解》 Hibernate作为Java领域中的一个强大的对象关系映射框架,提供了许多优化数据库操作的策略,其中之一便是延迟加载(Lazy Loading)。延迟加载机制旨在减少不必要的性能消耗,只在真正需要...

    Hibernate 延迟加载

    ### Hibernate延迟加载详解 #### 什么是Hibernate延迟加载? 在探讨Hibernate延迟加载之前,我们先简要回顾一下Hibernate框架。Hibernate是一种流行的Java持久层框架,它提供了对象关系映射(ORM)的功能,允许...

    hibernate延迟加载技术详细解

    ### Hibernate 延迟加载技术详解 #### 一、概述 在 Hibernate 框架中,延迟加载(Lazy Loading)是一种非常重要的优化技术,它能够有效地减少数据库查询次数,提高应用性能。通常,在多对多或者一对多的关系中,...

    Hibernate的延迟加载

    集合类型的延迟加载是Hibernate延迟加载机制中最能显著提升性能的部分。在Hibernate中,集合类型的延迟加载通过使用自定义的集合类实现,如`net.sf.hibernate.collection.Set`,而非标准的`java.util.Set`。这样,...

    hibernate延迟加载

    ### Hibernate延迟加载详解 #### 一、概述 Hibernate 是一种常用的 Java 持久层框架,它能够将 Java 应用程序与底层数据库之间建立桥梁,简化数据访问逻辑。为了提高应用程序性能,Hibernate 提供了多种优化手段,...

    Hibernate延迟加载

    Hibernate延迟加载是ORM框架Hibernate中的一个重要特性,它旨在提高应用程序的性能和效率。延迟加载(Lazy Loading)策略允许我们仅在需要访问一个对象或其属性时才从数据库加载它们,而不是在初始加载实体时就一次...

    Hibernate集合属性的延迟加载.doc

    在 Hibernate 框架中,延迟加载(Lazy Loading)是一种优化数据访问性能的重要技术。它允许我们只在真正需要数据时才从数据库加载,避免一次性加载大量数据导致的内存消耗和性能瓶颈。当我们处理与实体相关的集合...

    Flex 与 Hibernate 的延迟加载问题

    本文将深入探讨几种解决Flex与Hibernate延迟加载问题的方法,并着重讲解使用Gilead的方案。 1. **LCDS的Hibernate Adapter**:Adobe LiveCycle Data Services (LCDS) 提供了一个Hibernate适配器,可以处理延迟加载...

    Hibernate延迟加载案例 (多: 一: 一,附代码)

    本案例主要探讨的是Hibernate的延迟加载(Lazy Loading)机制,这是一种优化数据库访问性能的重要策略。延迟加载允许我们在需要数据时才去加载,而不是在初始化对象时一次性加载所有关联数据,从而减少了内存消耗和...

Global site tag (gtag.js) - Google Analytics