• hibernate延迟加载的原理与实现


    spring MVC + hibernate3 + spring的架构让我最头痛的就是hiberante3。后来项目因为数据量大,被迫改成了JDBC。现在回想当初那个hibernate3使用的相当菜了(现在似乎也算刚刚入门),而且对很多hibernate的概念原理懵懵懂懂,用的不好,招来老板对hibernate的一顿质疑。一年半后的今天。当我再次看待hibernate时,除了当年隐隐约约的“委屈”涌上心头,更希望让大家了解hibernate,不要仅仅从应用角度出发。好了,咱们今天来看看hibernate关于延迟加载的原理与实现。主要使用的就是CGLib。
     ====================
     
    首先看一段熟悉的代码:
     
    Java代码  
    public void testLazy() {  
        // 自己弄了一个丑陋的sessionFactory和session,主要是因为自己写的,比较容易控制。  
        SessionFactory<User, String> sessionFactory = new SessionFactoryImpl<User, String>(  
                User.class);  
        Session<User, String> session = sessionFactory.openSession();  
        User u = session.load("1");  
        // 这一句不会触发数据库查询操作,请看图1  
        assertEquals("1", u.getId());  
        // 访问的是非主键属性,开始查询数据库,请看图2  
        assertNotSame("11", u.getName());  
        session.close();  
    }  
    Java代码  
    public void testLazy() {  
        // 自己弄了一个丑陋的sessionFactory和session,主要是因为自己写的,比较容易控制。  
        SessionFactory<User, String> sessionFactory = new SessionFactoryImpl<User, String>(  
                User.class);  
        Session<User, String> 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代码  
    @Entity  
    public class User{  
        @Id  
        private String id;  
      
        @Column  
        private String name;  
      
        ........set,get省略  
    }  
    Java代码  
    @Entity  
    public class User{  
        @Id  
        private String id;  
      
        @Column  
        private String name;  
      
        ........set,get省略  
    }  
     
     这些@Entity,@Id,@Column也是我写的一些标注,让大家感觉更贴近hibernate(或jpa)些所做的一些模拟。所有的标注都是空实现,比如说@Id
     
    Java代码  
    @Retention(RetentionPolicy.RUNTIME)  
    @Target(ElementType.FIELD)  
    public @interface Id {  
      
    Java代码  
    @Retention(RetentionPolicy.RUNTIME)  
    @Target(ElementType.FIELD)  
    public @interface Id {  
      
    }
     
     
    这些标注在后面的反射操作中会用到。
     
     
    好现在我们从session.load方法慢慢深入
     
    Java代码  
    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<T, PK> interceptor = new LazyInitializerImpl<T, PK>();  
            // 将当前的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 + "]的实体");  
        }  
    Java代码  
    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<T, PK> interceptor = new LazyInitializerImpl<T, PK>();  
            // 将当前的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代码  
    public class LazyInitializerImpl<T, PK extends Serializable> implements  
            LazyInitializer<T, PK>, MethodInterceptor {  
      
        private Session<T, PK> 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);  
      
            }  
      
        }  
      
            ..............省略其它辅助方法  
      
    }  
    Java代码  
    public class LazyInitializerImpl<T, PK extends Serializable> implements  
            LazyInitializer<T, PK>, MethodInterceptor {  
      
        private Session<T, PK> 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操作上来说,会觉得这种设计确实是比较优雅。当然hibernate还有很多很多值得学习和借鉴的特性,下次有时间我再整理整理。
  • 相关阅读:
    最大似然估计
    信号和槽:Qt中最差劲的创造
    从生物神经网络到人工神经网络
    巩固一下C语言中的指针
    linux启动后自动登录并运行自定义图形界面程序
    删除linux系统服务
    《痞子衡嵌入式半月刊》 索引
    痞子衡嵌入式:恩智浦i.MX RTxxx系列MCU开发那些事
    痞子衡嵌入式:高性能MCU之音视频应用开发那些事
    痞子衡嵌入式:恩智浦i.MX RT1xxx系列MCU开发那些事
  • 原文地址:https://www.cnblogs.com/huapox/p/3251568.html
Copyright © 2020-2023  润新知