延迟加载(lazy load)也叫懒加载,它是Hibernate为提高程序执行效率而提供的一种机制,即只有真正使用该对象的数据时才会创建。
Hibernate中主要通过代理(proxy)机制来实现延迟加载的。具体过程:Hibernate从数据库获取某一个对象数据时、获取某一个对象的集合属性值时,或获取某一个对象所关联的另一对象时,由于没有使用该对象的数据(除标识符值外),Hibernate并不从数据库加载真正的数据,而只是为该对象创建一个代理对象来代表这个对象,这个对象上的所有属性都为默认值;只有在真正需要使用该对象的数据时才创建这个真实对象,真正从数据库中加载它的数据。这样在某些情况下,就可以提高查询效率。
Hibernate中默认采用延迟加载的情况主要由以下几种:
(1)当调用Session上的load()方法加载一个实体时,会采用延迟加载。
(2)当Session加载某个实体时,会对这个实体中的集合属性值采用延迟加载。
(3)当Session加载某个实体时,会对这个实体所单端关联的另一个实体对象采用延迟加载。
例如:如下程序代码
Account acc=(Account)session.load(Account.class,new Long(1));//返回的是一个代理对象
System.out.println(acc.getId());//没有发送SQL语句到数据库加载数据
System.out.println(acc.getLoginName());//创建真实的Account实例,并发送SQL语句到数据库中加载数据
解释:Session的load()方法对实体的加载默认采用延迟加载,而get()方法默认采用立即加载,所以第一行代码只返回一个代理对象,而第三行Hibernate才创建真实的Account实例。如果只访问对象标识符属性,它就没有必要初始化代理。
延迟加载确实会给程序的查询效率带来好处,但有时明确知道数据需要立即加载的,如果Hibernate先默认使用延迟加载,而后又必须去数据库加载,反而会降低效率。所以,需要根据应用程序的实际情况来灵活控制是否使用延迟加载。在Hibernate中只需要修改响应的配置来启用或关闭延迟加载功能:
(1)在加载单个实体,如果不需要延迟加载,就可以使用Session的get()方法。
(2)当Session加载某个实体时,不需要对这个实体中的集合属性值延迟加载,而是要立即加载。这时可以再映射文件中针对这个集合的配置元素(<set>、<bag>、<list>......)添加属性lazy=false。
(3)当Session家在某个实体时,不需要对这个实体所单端关联的另一个实体对象延迟加载,就可以在映射文件中针对这个单端关联的配置元素(<one-to-ong>、<many-to-one>)添加属性lazy=false。
咱们今天来看看hibernate关于延迟加载的原理与实现。主要使用的就是CGLib。
首先看一段熟悉的代码:
- public void testLazy() {
-
- SessionFactory<User, String> sessionFactory = new SessionFactoryImpl<User, String>(
- User.class);
- Session<User, String> session = sessionFactory.openSession();
- User u = session.load("1");
-
- assertEquals("1", u.getId());
-
- assertNotSame("11", u.getName());
- session.close();
- }
- <span style="font-size:18px;"> public void testLazy() {
-
- SessionFactory<User, String> sessionFactory = new SessionFactoryImpl<User, String>(
- User.class);
- Session<User, String> session = sessionFactory.openSession();
- User u = session.load("1");
-
- assertEquals("1", u.getId());
-
- assertNotSame("11", u.getName());
- session.close();
- }</span>
图1:通过断点,我们可以看到User对象只是一个代理,并且只有主键id有值
图2:通过断点,我们可以看到原本属于代理对象的User,其targetObject一项已经有值了,表示已经发出select语句从数据库取值了。
好,有了这点感性认识,咱们继续前进。
原理:在hibernate中,如果使用了延迟加载(比如常见的load方法),那么除访问主键以外的其它属性时,就会去访问数据库(假设不考虑hibernate的一级缓存),此时session是不允许被关闭。
先简单看看要操作的对象User
- @Entity
- public class User{
- @Id
- private String id;
-
- @Column
- private String name;
-
- ........set,get省略
- }
- <span style="font-size:18px;">@Entity
- public class User{
- @Id
- private String id;
-
- @Column
- private String name;
-
- ........set,get省略
- }
- </span>
这些@Entity,@Id,@Column也是我写的一些标注,让大家感觉更贴近hibernate(或jpa)些所做的一些模拟。所有的标注都是空实现,比如说@Id
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.FIELD)
- public @interface Id {
-
- }
- <span style="font-size:18px;">@Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.FIELD)
- public @interface Id {
-
- }
- </span>
这些标注在后面的反射操作中会用到。
好现在我们从session.load方法慢慢深入
- public T load(PK id) {
-
-
- final FieldClass fieldClass = annotationParas.generatorSQL(type);
- T obj = null;
-
- LazyInitializer<T, PK> interceptor = new LazyInitializerImpl<T, PK>();
-
-
- interceptor.setSession(this);
-
-
-
EnhancerByCGLib
- Enhancer enhancer = new Enhancer();
- enhancer.setSuperclass(type);
-
- enhancer.setCallback(interceptor);
- obj = (T) enhancer.create();
-
- try {
-
-
-
- Method method = type.getMethod(getMethodFromField(fieldClass
- .getKey()),
- new Class<?>[] { fieldClass.getKey().getType() });
- method.invoke(obj, new Object[] { id });
- return obj;
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- throw new RuntimeException("找不到主键为:[" + id + "]的实体");
- }
- <span style="font-size:18px;">public T load(PK id) {
-
-
- final FieldClass fieldClass = annotationParas.generatorSQL(type);
- T obj = null;
-
- LazyInitializer<T, PK> interceptor = new LazyInitializerImpl<T, PK>();
-
-
- interceptor.setSession(this);
-
-
-
EnhancerByCGLib
- Enhancer enhancer = new Enhancer();
- enhancer.setSuperclass(type);
-
- enhancer.setCallback(interceptor);
- obj = (T) enhancer.create();
-
- try {
-
-
-
- Method method = type.getMethod(getMethodFromField(fieldClass
- .getKey()),
- new Class<?>[] { fieldClass.getKey().getType() });
- method.invoke(obj, new Object[] { id });
- return obj;
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- throw new RuntimeException("找不到主键为:[" + id + "]的实体");
- }</span>
annotationParas其实就是一个工具类,完成实体类与数据库表之间的映射。里面无非就是反射,判断,组装,最后组成一个我们想要的数据信息装进一个载体里——在这里是一个叫FieldClass 的JavaBean。对hibernate来说,将对象映射工作是在程序启动之初就完成了。
接下来是LazyInitializer,咱们先看它的实现:
- public class LazyInitializerImpl<T, PK extends Serializable> implements
- LazyInitializer<T, PK>, MethodInterceptor {
-
- private Session<T, PK> session;
- private boolean isAlreadyInit = false;
- private T targetObject;
-
-
- public Object intercept(Object obj, Method method, Object[] args,
- MethodProxy proxy) throws Throwable {
-
- Class<?> clas = obj.getClass();
- Field field = getPrimaryKey(clas);
-
- assert (field != null);
-
-
- if (method.getName().toLowerCase().indexOf(field.getName()) > -1) {
- return proxy.invokeSuper(obj, args);
- } else {
- if (!isAlreadyInit) {
- field.setAccessible(true);
-
- targetObject = session.get((PK) field.get(obj));
- isAlreadyInit = true;
- }
- return method.invoke(targetObject, args);
-
- }
-
- }
-
- ..............省略其它辅助方法
-
- }
- <span style="font-size:18px;">public class LazyInitializerImpl<T, PK extends Serializable> implements
- LazyInitializer<T, PK>, MethodInterceptor {
-
- private Session<T, PK> session;
- private boolean isAlreadyInit = false;
- private T targetObject;
-
-
- public Object intercept(Object obj, Method method, Object[] args,
- MethodProxy proxy) throws Throwable {
-
- Class<?> clas = obj.getClass();
- Field field = getPrimaryKey(clas);
-
- assert (field != null);
-
-
- if (method.getName().toLowerCase().indexOf(field.getName()) > -1) {
- return proxy.invokeSuper(obj, args);
- } else {
- if (!isAlreadyInit) {
- field.setAccessible(true);
-
- targetObject = session.get((PK) field.get(obj));
- isAlreadyInit = true;
- }
- return method.invoke(targetObject, args);
-
- }
-
- }
-
- ..............省略其它辅助方法
-
- }</span>
当我们User u = session.load("1")对象后,
- 调用u.getId()时,会立即转入LazyInitializer的intercept()方法,然后按照上面的逻辑,自然是直接返回getId()的值,根本不会与数据库打交道。
- 当调用u.getName()时,也会先立即转入LazyInitializer的intercept()方法,然后发现"getName()".indexOf("id")>-1==false,于是立即利用已经绑定的session对象去用主键ID往数据库里查询。这也是为什么在hibernate中,如果使用了延迟加载使得一个代理没有被初始化,而你又关闭了session,再次去取除主键外的其它属性时,常常出现session close异常。
看到这里,大家是不是觉得所谓的延迟加载并不是那么神秘,而且从数据库I/O操作上来说,会觉得这种设计确实是比较优雅
hibernate的延迟加载
原文:http://www.cnblogs.com/cxxjohnson/p/4951582.html