一、延迟加载简介
当Hibernate从数据库中初始化某个持久态实体时,如果集合属性里包含十几万、甚至百万条记录,在初始化持久态实体的同时,完成所有集合属性的抓取,将导致性能急剧下降。有可能系统只需要持久态实体集合属性中的部分记录而不是全部记录,这样,没必要一次加载所有的集合属性。
对于集合属性,通常推荐使用延迟加载策略。所谓延迟加载策略就是等系统需要集合属性时才从数据库装在关联的数据。通过延迟加载技术可以避免过多、过早地加载数据表中的数据,从而降低内存开销。
二、延迟加载的本质
延迟加载的本质上就是代理模式的应用,当程序通过Hibernate装载一个实体时,默认情况下,Hibernate并不会立即抓取它的属性集合、关联属性所对应记录,而是通过生成一个代理来表示这些集合属性、关联实体,这个代理仅仅保存了id这个属性。
三、get和load
在Hibernate中如果我们要从数据库获取一个对象,通常有两种方式,一种是通过get(),另一种就是通过load()。get和load方法都要通过主键进行查询,其他字段不能够使用。
(1)get加载方式
当我们使用session.get()方法来获取一个对象时,不管我们是不是用这个对象,此时都会发出sql语句去从数据库中查询出来。
//通过get方法来加载对象时,不管使不使用该对象,都会发出sql语句,从数据库中查询
User user = (User)session.get(User.class, 2);
此时我们通过get方式来得到user对象,但是我们并没有使用它(比如user.getId()),但是我们发现控制台会输出sql的查询语句:
Hibernate: select user0_.id as id0_0_, user0_.username as username0_0_, user0_.password as password0_0_, user0_.born as born0_0_ from user user0_ where user0_.id=?
(2)load加载方式
当使用load方法来得到一个对象时,此时hibernate会使用延迟加载的机制来加载这个对象,也就是不会发出sql语句。load方法得到的对象其实是一个代理对象,这个代理对象只保存了实体对象的id值。只有当我们要使用这个对象,得到其他属性时,这个时候才会发出sql语句,从数据库中去查询我们的对象。
//此时并不会发出sql语句,只有当我们需要使用的时候才会从数据库中去查询
User user = (User)session.load(User.class, 2);
我们看到,如果我们仅仅是通过load来加载我们的User对象,此时从控制台我们会发现并不会从数据库中查询出该对象,即并不会发出sql语句,但如果我们要使用该对象时:
session = HibernateUtil.openSession();
User user = (User)session.load(User.class, 2);
System.out.println(user);
此时我们看到控制台会发出了sql查询语句,会将该对象从数据库中查询出来:
Hibernate: select user0_.id as id0_0_, user0_.username as username0_0_, user0_.password as password0_0_, user0_.born as born0_0_ from user user0_ where user0_.id=?
User [id=2, username=aaa, password=111, born=2013-10-16 00:14:24.0]
这个时候我们可能会想,那么既然调用load方法时,并不会发出sql语句去从数据库中查出该对象,那么这个User对象到底是个什么对象呢?
答案揭晓!User对象是我们的一个代理对象,这个代理对象仅仅保存了id这个属性。
session = HibernateUtil.openSession();
User user = (User)session.load(User.class, 2);
System.out.println(user.getId());
所以如果我们只打印出这个user对象的id值时,此时控制台会打印出该id值,但是同样不会发出sql语句去从数据库中去查询。
(3)get和load方法的区别
①当我们试图得到一个id不存在的对象时,get和load的返回结果不同:如果使用get方式来加载对象,会返回null,当我们使用这个对象的时候就会抛出NullPointException异常;而如果使用load方式来加载对象,会返回一个代理对象,当我们使用这个对象的时候会报ObjectNotFoundException异常。
那么这是为什么呢?是因为get()方法直接返回实体类,如果查不到数据则返回null。load()会返回一个实体代理对象(当前这个对象可以自动转化为实体对象),hibernate认为该id对应的对象(数据库记录)在数据库中是一定存在的,所以它可以放心的使用,它可以放心的使用代理来 延迟加载该对象。在用到对象中的其他属性数据时才查询数据库,但是万一数据库中不存在该记录,那没办法,只能抛异常。
session = HibernateUtil.openSession();
//返回null,此时不抛异常
User user = (User)session.get(User.class, 20);
//抛出NullPointException异常
System.out.println(user.getUsername());
session = HibernateUtil.openSession();
//返回代理对象,此时不抛异常
User user = (User)session.load(User.class, 20);
//抛出ObjectNotFoundException异常
System.out.println(user.getUsername());
②load方法和get方法查询对象的过程不同。
Hibernate维持了两级缓存。第一级缓存是session缓存,也称为内部缓存,由hibernate管理,其中保存了session当前所有关联实体的数据;第二级缓存是SessionFactory级别的全局缓存,它是属于进程范围或群集范围的缓存,如Ehcache,这一级别的缓存可以进行配置和更改,并且可以动态加载和卸载。
这里我们看一下session加载对象的过程:出于性能考虑,session在调用数据库查询功能之前,会先在第一级缓存中进行查询。如果第一级缓存查找命中,且数据状态合法,则直接返回。如果不命中,session会在当前查询黑名单列表(记录了当前session实例在之前所有查询操作中,未能查询到有效数据的查询条件)记录中查找,如果记录中存在同样的查询条件,则返回null(既然查不到就没必要查下去了)。对于load和get方法,如果缓存中查找不到数据,则发起数据库查询操作,如经过查询未发现对应记录,则将此次查询的信息在“查询黑名单列表”中加以记录,并返回null。
然后我们看一下get和load方法加载对象的过程:
- get先到缓存(session缓存/二级缓存)中去查看该id对应的对象是否存在,如果没有就到DB中去查(即马上发出sql)
- load方法创建时首先查询session缓存看看该id对应的对象是否存在,没有则判断是否是lazy,如果不是直接访问数据库检索,查到记录返回,是lazy的话就创建代理(不马上到DB中去找),实际使用数据时才查询二级缓存和数据库。
对于load和get方法返回类型:"get()永远只返回实体类",但实际上这是不正确的。get方法如果在session缓存中找到了该id对应的对象,如果刚好该对象前面是被代理过的,如被load方法使用过,或者被其他关联对象延迟加载过,那么返回的还是原先的代理对象,而不是实体类对象。如果该代理对象还没有加载实体数据(就是id以外的其他属性数据),那么在用到这个对象的时候,它会查询二级缓存或者数据库来加载数据(即不会立即查询数据库或者二级缓存),但是返回的还是代理对象,只不过已经加载了实体数据。(代理对象实际就是空的对象,并没有去数据库查询数据;如果去数据库查询了 返回到了这个对象 ,我们叫实体对象)
十分注意,Hibernate虽然允许对关联对象、属性进行延迟加载,但是必须保证延迟加载的操作限于同一个 Hibernate Session范围之内进行。同时要遵守一个请求一个Hibernate session的原则。
四、延迟加载和关联查询
默认情况下,Hibernate也会采用延迟加载来加载关联实体,不管是一对多关联、还是一对一关联、多对多关联,Hibernate 默认都会采用延迟加载。Hibernate采用“延迟加载”管理关联实体的模式时,select在查询时只会查出主表记录,用到了关联数据时再自动在执行查询,也就是在加载主实体时,并未真正去抓取关联实体对应数据,而只是动态地生成一个对象作为关联实体的代理。
五、使用延迟加载带来的问题
使用延迟加载最经常出现的就是LazyInitializationException异常。
我们来看一个例子:
public class UserDAO
{
public User loadUser(int id)
{
Session session = null;
Transaction tx = null;
User user = null;
try
{
session = HibernateUtil.openSession();
tx = session.beginTransaction();
user = (User)session.load(User.class, 1);
tx.commit();
}
catch (Exception e)
{
e.printStackTrace();
tx.rollback();
}
finally
{
HibernateUtil.close(session);
}
return user;
}
}
@Test
public void testLazy()
{
UserDAO userDAO = new UserDAO();
User user = userDAO.loadUser(2);
System.out.println(user.getName());
}
运行测试用例,控制台会抛出如下错误:
org.hibernate.LazyInitializationException: could not initialize proxy - no Session .............
产生这个异常的原因是当我们通过load方法加载一个对象时,并没有发出sql语句去从数据库中查询出该对象,当前这个对象仅仅是一个代理对象,我们并没有使用它,但是此时我们的session已经关闭了,所以当我们在测试用例中使用该对象时就会报LazyInitializationException这个异常了。
解决这个问题的方法有以下几种:
a.将load改成get的方式来得到该对象
这个解决办法相当于关闭延迟加载,但是这样带来的隐患是十分大的。从理论的角度讲,最好是用一个就关一个,防止资源消耗。如果关闭了延迟加载,那么在关联查询中,如果关联表越多,则每次查询的开销就越大。所以不建议使用这个方法解决。
b.使用OpenSessionInViewFilter/OpenSessionInViewInterceptor
OpenSessionInViewFilter是Spring提供的一个针对Hibernate的一个支持类,其主要意思是在发起一个页面请求时打开Hibernate的Session,一直保持这个Session,直到这个请求结束,具体是通过一个Filter来实现的。如果应用中使用了OpenSessionInViewFilter或者OpenSessionInViewInterceptor,所有打开的 session会被保存在一个线程变量里。在线程退出前通过OpenSessionInViewFilter或者OpenSessionInViewInterceptor断开这些session。
在OpenSessionInView的配置中,singleSession应该设置为true,表示一个request只能打开一个 session(符合Hibernate一个请求一个session的原则),如果设置为false的话,session可以被打开多个,这时在update、delete的时候会出现打开多个session的异常。
使用OpenSessionInViewFilter确实可以降低延迟加载所引发的的各种问题,使Service层代码更易开发和维护。但是对于大型且高并发的应用来说,强烈建议不要使用OpenSessionInViewFilter,因为OpenSessionInViewFilter会让每个web请求线程都绑定一个Hibernate的Session,即会绑定一个数据连接,直到完成web请求处理时才释放数据连接。这会造成两个性能问题:
- 加大对数据连接资源访问的并发性(因为原本有些web请求可能并不需要使用到数据库连接);
- 延长了每个web请求对数据连接资源占用的时长。由于OpenSessionInViewFilter把session绑在当前线程上,导致session的生命周期比事务要长,这期间所有事务性操作都在复用这同一个session,由此可能会产生了一些“怪问题”。
应用系统的瓶颈往往都出现在数据库上,使用OpenSessionInViewFilter会使系统更容易出现数据库资源的性能瓶颈。OpenSessionInViewFilter调用流程为:request(请求)->open session并开始transaction->controller->View(Jsp)->结束transaction并close session。我们试想下如果流程中的某一步被阻塞的话,那在这期间connection就一直被占用而不释 放。最有可能被阻塞的就是在写Jsp这步,一方面可能是页面内容大,response.write()的时间长,另一方面可能是网速慢,服务器与用户间传输时 间久。当大量这样的情况出现时,就有连接池连接不足,造成页面假死现象。
c.设计好Service接口
开发者必须要设计好Service接口,使延迟加载的工作在Service层内完成,也就是尽量在Service下载完所有的东西。此外,还要求提供满足不同需求的Service接口。假设User和Dept两个实体对象是Many-to-One的关系,User中的Dept使用了延迟加载方案,那么UserService 需要提供两个不同的获取User对象的服务接口,以应对不同的应用场景:其一为getUserWithDept(),返回带Dept的User;其二为getUserWithoutDept(),返回不带Dept的User。
d.通过ThreadLocal来处理session
private static ThreadLocal<Session> sessionHolder = new ThreadLocal<Session>();
private static void setSession(Session session) {
sessionHolder.set(session);
}
public static Session getSession() {
return sessionHolder.get();
}
private static void removeSession() {
sessionHolder.remove();
}