Hibernate延迟加载

      延迟加载(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加载某个实体时,不需要对这个实体中的集合属性值延迟加载,而是要立即加载。这时可以再映射文件中针对这个集合的配置元素(......)添加属性lazy=false。

  (3)当Session家在某个实体时,不需要对这个实体所单端关联的另一个实体对象延迟加载,就可以在映射文件中针对这个单端关联的配置元素()添加属性lazy=false。

 

咱们今天来看看hibernate关于延迟加载的原理与实现。主要使用的就是CGLib。

 

 

首先看一段熟悉的代码:

 

Java代码
  1. public void testLazy() {   
  2.     // 自己弄了一个丑陋的sessionFactory和session,主要是因为自己写的,比较容易控制。  
  3.     SessionFactory sessionFactory = new SessionFactoryImpl(   
  4.             User.class);   
  5.     Session session = sessionFactory.openSession();   
  6.     User u = session.load("1");   
  7.     // 这一句不会触发数据库查询操作,请看图1  
  8.     assertEquals("1", u.getId());   
  9.     // 访问的是非主键属性,开始查询数据库,请看图2  
  10.     assertNotSame("11", u.getName());   
  11.     session.close();   
  12. }  
	public void testLazy() {
		// 自己弄了一个丑陋的sessionFactory和session,主要是因为自己写的,比较容易控制。
		SessionFactory sessionFactory = new SessionFactoryImpl(
				User.class);
		Session session = sessionFactory.openSession();
		User u = session.load("1");
		// 这一句不会触发数据库查询操作,请看图1
		assertEquals("1", u.getId());
		// 访问的是非主键属性,开始查询数据库,请看图2
		assertNotSame("11", u.getName());
		session.close();
	}

 

 图1:通过断点,我们可以看到User对象只是一个代理,并且只有主键id有值

 

图2:通过断点,我们可以看到原本属于代理对象的User,其targetObject一项已经有值了,表示已经发出select语句从数据库取值了。

好,有了这点感性认识,咱们继续前进。

 

原理:在hibernate中,如果使用了延迟加载(比如常见的load方法),那么除访问主键以外的其它属性时,就会去访问数据库(假设不考虑hibernate的一级缓存),此时session是不允许被关闭。 

 

先简单看看要操作的对象User

 

Java代码
  1. @Entity  
  2. public class User{   
  3.     @Id  
  4.     private String id;   
  5.   
  6.     @Column  
  7.     private String name;   
  8.   
  9.     ........set,get省略   
  10. }  
@Entity
public class User{
	@Id
	private String id;

	@Column
	private String name;

	........set,get省略
}

 

 这些@Entity,@Id,@Column也是我写的一些标注,让大家感觉更贴近hibernate(或jpa)些所做的一些模拟。所有的标注都是空实现,比如说@Id

 

Java代码
  1. @Retention(RetentionPolicy.RUNTIME)   
  2. @Target(ElementType.FIELD)   
  3. public @interface Id {   
  4.   
  5. }  
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Id {

}

 

这些标注在后面的反射操作中会用到。

 

 

好现在我们从session.load方法慢慢深入

 

Java代码
  1. public T load(PK id) {   
  2.         // annotationParas利用反射解析被标注为@Entity的type类型(比如说上文提到的User.class),  
  3.         // 然后将标注为@Id和@Column的属性存入FieldClass对象,供下面进一步使用  
  4.         final FieldClass fieldClass = annotationParas.generatorSQL(type);   
  5.         T obj = null;   
  6.         // 因为是load方法,默认给它加一个基于CGLib的拦截器,该拦截器是实现延迟加载的关键,稍后我们再详细看看  
  7.         LazyInitializer interceptor = new LazyInitializerImpl();   
  8.         // 将当前的session对象设置给该拦截器,以便在取非主键属性时,能够正常查询数据库  
  9.         // 从而将对象初始化  
  10.         interceptor.setSession(this);   
  11.   
  12.         // 默认生成的是一个基于CGLib的代理,并非真实的对象,通过图1,图2,大家应该可以看到  
  13.         // User=User$$EnhancerByCGLib$$... 我就不多说了  
  14.         Enhancer enhancer = new Enhancer();   
  15.         enhancer.setSuperclass(type);   
  16.         // 注意别忘记将刚才生成的拦截器注入到代理中去  
  17.         enhancer.setCallback(interceptor);   
  18.         obj = (T) enhancer.create();   
  19.   
  20.         try {   
  21.             // 因为通过CGLib生成的User对象,主键属性id=null  
  22.             // 所以我们还得执行主键的set方法(比如说setId),这样就可以像图1显示那样,id="1"是有值的  
  23.             // 到此,load方法执行完毕,始终没有查询数据库  
  24.             Method method = type.getMethod(getMethodFromField(fieldClass   
  25.                     .getKey()),   
  26.                     new Class[] { fieldClass.getKey().getType() });   
  27.             method.invoke(obj, new Object[] { id });   
  28.             return obj;   
  29.         } catch (Exception e) {   
  30.             e.printStackTrace();   
  31.         }   
  32.   
  33.         throw new RuntimeException("找不到主键为:[" + id + "]的实体");   
  34.     }  
public T load(PK id) {
		// annotationParas利用反射解析被标注为@Entity的type类型(比如说上文提到的User.class),
		// 然后将标注为@Id和@Column的属性存入FieldClass对象,供下面进一步使用
		final FieldClass fieldClass = annotationParas.generatorSQL(type);
		T obj = null;
		// 因为是load方法,默认给它加一个基于CGLib的拦截器,该拦截器是实现延迟加载的关键,稍后我们再详细看看
		LazyInitializer interceptor = new LazyInitializerImpl();
		// 将当前的session对象设置给该拦截器,以便在取非主键属性时,能够正常查询数据库
		// 从而将对象初始化
		interceptor.setSession(this);

		// 默认生成的是一个基于CGLib的代理,并非真实的对象,通过图1,图2,大家应该可以看到
		// User=User$$EnhancerByCGLib$$... 我就不多说了
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(type);
		// 注意别忘记将刚才生成的拦截器注入到代理中去
		enhancer.setCallback(interceptor);
		obj = (T) enhancer.create();

		try {
			// 因为通过CGLib生成的User对象,主键属性id=null
			// 所以我们还得执行主键的set方法(比如说setId),这样就可以像图1显示那样,id="1"是有值的
			// 到此,load方法执行完毕,始终没有查询数据库
			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 + "]的实体");
	}

 

 

annotationParas其实就是一个工具类,完成实体类与数据库表之间的映射。里面无非就是反射,判断,组装,最后组成一个我们想要的数据信息装进一个载体里——在这里是一个叫FieldClass 的JavaBean。对hibernate来说,将对象映射工作是在程序启动之初就完成了。

 

接下来是LazyInitializer,咱们先看它的实现:

 

 

Java代码
  1. public class LazyInitializerImplextends Serializable> implements  
  2.         LazyInitializer, MethodInterceptor {   
  3.   
  4.     private Session session; // 绑定的session对象  
  5.     private boolean isAlreadyInit = false// 是否已经查询过数据库  
  6.     private T targetObject; // 目标对象  
  7.   
  8.     // 通CGLib生成的对象,如果设置了此拦截器,那么其方法每次调用时,都会触发此方法  
  9.     public Object intercept(Object obj, Method method, Object[] args,   
  10.             MethodProxy proxy) throws Throwable {   
  11.         // 继续利用反射得到代理对象的标有@Id的主键属性  
  12.         Class clas = obj.getClass();   
  13.         Field field = getPrimaryKey(clas);   
  14.   
  15.         assert (field != null);   
  16.         // 如果当前调用的方法是标注为@Id的话,那么就不从数据库里取,直接返回代理  
  17.         // 即如果是getId()的话,直接用代理调用;如果是getName()的话,那就必须查询数据库,取出实际对象,并进行相应的调用了  
  18.         if (method.getName().toLowerCase().indexOf(field.getName()) > -1) {   
  19.             return proxy.invokeSuper(obj, args);   
  20.         } else {   
  21.             if (!isAlreadyInit) {   
  22.                 field.setAccessible(true);   
  23.                 // session.get方法直接查询数据库,并将ResultSet结果组将成User对象  
  24.                 targetObject = session.get((PK) field.get(obj));   
  25.                 isAlreadyInit = true;   
  26.             }   
  27.             return method.invoke(targetObject, args);   
  28.   
  29.         }   
  30.   
  31.     }   
  32.   
  33.         ..............省略其它辅助方法   
  34.   
  35. }  
public class LazyInitializerImpl implements
		LazyInitializer, MethodInterceptor {

	private Session session; // 绑定的session对象
	private boolean isAlreadyInit = false; // 是否已经查询过数据库
	private T targetObject; // 目标对象

	// 通CGLib生成的对象,如果设置了此拦截器,那么其方法每次调用时,都会触发此方法
	public Object intercept(Object obj, Method method, Object[] args,
			MethodProxy proxy) throws Throwable {
		// 继续利用反射得到代理对象的标有@Id的主键属性
		Class clas = obj.getClass();
		Field field = getPrimaryKey(clas);

		assert (field != null);
		// 如果当前调用的方法是标注为@Id的话,那么就不从数据库里取,直接返回代理
		// 即如果是getId()的话,直接用代理调用;如果是getName()的话,那就必须查询数据库,取出实际对象,并进行相应的调用了
		if (method.getName().toLowerCase().indexOf(field.getName()) > -1) {
			return proxy.invokeSuper(obj, args);
		} else {
			if (!isAlreadyInit) {
				field.setAccessible(true);
				// session.get方法直接查询数据库,并将ResultSet结果组将成User对象
				targetObject = session.get((PK) field.get(obj));
				isAlreadyInit = true;
			}
			return method.invoke(targetObject, args);

		}

	}

        ..............省略其它辅助方法

}

 

 

当我们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操作上来说,会觉得这种设计确实是比较优雅

 

你可能感兴趣的:(面试,J2EE)