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

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

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

    首先看一段熟悉的代码:

    Java代码
    1. public void testLazy() {   
    2.     // 自己弄了一个丑陋的sessionFactory和session,主要是因为自己写的,比较容易控制。  
    3.     SessionFactory<User, String> sessionFactory = new SessionFactoryImpl<User, String>(   
    4.             User.class);   
    5.     Session<User, String> 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. }  
    [java] view plaincopy
     
    1. <span style="font-size:18px;">  public void testLazy() {  
    2.         // 自己弄了一个丑陋的sessionFactory和session,主要是因为自己写的,比较容易控制。  
    3.         SessionFactory<User, String> sessionFactory = new SessionFactoryImpl<User, String>(  
    4.                 User.class);  
    5.         Session<User, String> 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.     }</span>  

     图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. }  
    [java] view plaincopy
     
    1. <span style="font-size:18px;">@Entity  
    2. public class User{  
    3.     @Id  
    4.     private String id;  
    5.   
    6.     @Column  
    7.     private String name;  
    8.   
    9.     ........set,get省略  
    10. }  
    11. </span>  

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

    Java代码
    1. @Retention(RetentionPolicy.RUNTIME)   
    2. @Target(ElementType.FIELD)   
    3. public @interface Id {   
    4.   
    5. }  
    [java] view plaincopy
     
    1. <span style="font-size:18px;">@Retention(RetentionPolicy.RUNTIME)  
    2. @Target(ElementType.FIELD)  
    3. public @interface Id {  
    4.   
    5. }  
    6. </span>  

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

    好现在我们从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<T, PK> interceptor = new LazyInitializerImpl<T, PK>();   
    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.     }  
    [java] view plaincopy
     
    1. <span style="font-size:18px;">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<T, PK> interceptor = new LazyInitializerImpl<T, PK>();  
    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.     }</span>  

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

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

    Java代码
    1. public class LazyInitializerImpl<T, PK extends Serializable> implements  
    2.         LazyInitializer<T, PK>, MethodInterceptor {   
    3.   
    4.     private Session<T, PK> 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. }  
    [java] view plaincopy
     
    1. <span style="font-size:18px;">public class LazyInitializerImpl<T, PK extends Serializable> implements  
    2.         LazyInitializer<T, PK>, MethodInterceptor {  
    3.   
    4.     private Session<T, PK> 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. }</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操作上来说,会觉得这种设计确实是比较优雅

  • 相关阅读:
    C++ 重载操作符- 01 简单的入门
    C++ 析构函数
    Auto Control 001 自动控制的一般概念
    C++ 友元
    安装 SQL Server 2014 Express
    关闭是否只查看安全传送的网页内容提示框 和 是否允许运行软件,如ActiveX控件和插件提示框
    Python 网络爬虫 010 (高级功能) 解析 robots.txt 文件
    2019-06-12 Java学习日记之JDBC
    2019-06-11 Java学习日记之Bootstrap
    2019-06-10 Java学习日记之JQuery
  • 原文地址:https://www.cnblogs.com/cxxjohnson/p/4951582.html
Copyright © 2011-2022 走看看