Mybatis源码剖析 -- 延迟加载

一、什么是延迟加载

  1. 在开发过程中,假设有一个用户信息类,映射多个订单信息类
    • 立即加载:如果每次加载用户信息的同时就加载这个用户下的所有订单信息,那么这就叫做立即加载
    • 延迟加载:查询用户信息的时候仅仅只查询用户信息,等什么时候需要用到其订单信息的时候再去查询这个用户下的所有订单信息,这就叫延迟加载
  2. 举个例子
    • 问题
      在一对多中,当我们有⼀个用户,它有个100个订单
      在查询用户的时候,要不要把关联的订单查出来?
      在查询订单的时候,要不要把关联的用户查出来?
    • 回答
      在查询用户时,用户下的订单应该是什么时候用,什么时候查询
      在查询订单时,订单所属的用户信息应该是随着订单⼀起查询出来的
  3. 延迟加载
    就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
    • 优点
      先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能
    • 缺点
      因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降
    • 在多表中
      ⼀对多,多对多:通常情况下采用延迟加载
      ⼀对⼀(多对⼀):通常情况下采用立即加载
    • 注意
      延迟加载是基于嵌套查询来实现的

二、延迟加载的应用

  1. 局部延迟加载
    在 association 和 collection 标签中都有⼀个 fetchType 属性,通过修改它的值,可以修改局部的加载策略
    • 懒加载策略:fetchType="lazy"
    • 立即加载策略:fetchType="eager"
  2. 全局延迟加载
    在 Mybatis 的核心配置文件中可以使用 setting 标签修改全局的加载策略
    <settings>
        <!--开启全局延迟加载功能-->
        <setting name="lazyLoadingEnabled" value="true"/>
    </settings>
    
  3. 注意:局部的加载策略 高于 全局的加载策略

三、延迟加载的实现原理

其实延迟加载的底层原理:还是动态代理,通过代理拦截到指定方法,执行数据加载

  • 使用 CGLIB 或 Javassist(默认)创建目标对象的代理对象
  • 当调用代理对象的延迟加载属性的 getting 方法时,进入拦截器方法
  • 比如调用 a.getB().getName() 方法,进入拦截器的invoke(...) 方法,发现 a.getB() 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B对象的 SQL ,把 B 查询出来,然后调用 a.setB(b) 方法,于是 a 对象 b 属性就有值了,接着完成 a.getB().getName() 方法的调用

四、延迟加载源码剖析 -- 创建代理对象

  1. Setting 配置加载
    /**
     * MyBatis 配置
     *
     * @author Clinton Begin
     */
    public class Configuration {
        /**
         * 当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载(参考lazyLoadTriggerMethods)
         */
        protected boolean aggressiveLazyLoading;
    
        /**
         * 延迟加载触发⽅法
         */
         protected Set<String> lazyLoadTriggerMethods = new HashSet<String> (Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
         /** 是否开启延迟加载 */
         protected boolean lazyLoadingEnabled = false;
    
         /**
          * 延迟加载代理对象创建 Mybatis 的查询结果是由 ResultSetHandler 接口的 handleResultSets() 方法处理的,ResultSetHandler 接口只有⼀个实现,DefaultResultSetHandler,接下来看下延迟加载相关的⼀个核心的方法
          * 默认使⽤Javassist代理⼯⼚
          * @param proxyFactory
          */
          public void setProxyFactory(ProxyFactory proxyFactory) {
              if (proxyFactory == null) {
                  proxyFactory = new JavassistProxyFactory();
              }
              this.proxyFactory = proxyFactory;
          }
    
          //省略...
    }
    
  2. 代理对象创建
    Mybatis 的查询结果是由 ResultSetHandler 接口的handleResultSets()方法处理的,ResultSetHandler 接口只有⼀个实现,DefaultResultSetHandler,接下来看下延迟加载相关的⼀个核心的方法
    // 创建映射后的结果对象
    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
        // useConstructorMappings ,表示是否使用构造方法创建该结果对象。此处将其重置
        this.useConstructorMappings = false; // reset previous mapping result
        final List<Class<?>> constructorArgTypes = new ArrayList<>(); // 记录使用的构造方法的参数类型的数组
        final List<Object> constructorArgs = new ArrayList<>(); // 记录使用的构造方法的参数值的数组
        // 创建映射后的结果对象
        Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
        if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
            // 如果有内嵌的查询,并且开启延迟加载,则创建结果对象的代理对象
            final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
            for (ResultMapping propertyMapping : propertyMappings) {
                // issue gcode #109 && issue #149
                if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                    resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
                    break;
                }
            }
        }
        // 判断是否使用构造方法创建该结果对象
        this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
        return resultObject;
    }
    
  3. 当设置了fetchType="lazy"时,会执行configuration.getProxyFactory().createProxy(...),而 configuration.getProxyFactory()就是protected ProxyFactory proxyFactory = new JavassistProxyFactory();,其实就是 new 了一个 JavassistProxyFactory,所以说默认就是 JavassistProxyFactory 去生成代理对象
  4. 创建代理对象
    public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        final Class<?> type = target.getClass();
        // 创建 EnhancedResultObjectProxyImpl 对象
        EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
        // 创建代理对象
        Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
        // 将 target 的属性,复制到 enhanced 中
        PropertyCopier.copyBeanProperties(type, target, enhanced);
        return enhanced;
    }
    
    static Object crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        // 创建 javassist ProxyFactory 对象
        ProxyFactory enhancer = new ProxyFactory();
        // 设置父类
        enhancer.setSuperclass(type);
    
        // 根据情况,设置接口为 WriteReplaceInterface 。和序列化相关,可以无视
        try {
            type.getDeclaredMethod(WRITE_REPLACE_METHOD); // 如果已经存在 writeReplace 方法,则不用设置接口为 WriteReplaceInterface
            // ObjectOutputStream will call writeReplace of objects returned by writeReplace
            if (log.isDebugEnabled()) {
                log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
            }
        } catch (NoSuchMethodException e) {
            enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class}); // 如果不存在 writeReplace 方法,则设置接口为 WriteReplaceInterface
        } catch (SecurityException e) {
            // nothing to do here
        }
    
        // 创建代理对象
        Object enhanced;
        Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
        Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
        try {
            enhanced = enhancer.create(typesArray, valuesArray);
        } catch (Exception e) {
            throw new ExecutorException("Error creating lazy proxy.  Cause: " + e, e);
        }
    
        // 设置代理对象的执行器
        ((Proxy) enhanced).setHandler(callback);
        return enhanced;
    }
    

五、延迟加载源码剖析 -- invoke方法执行

@Override
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
    final String methodName = method.getName();
    try {
        synchronized (lazyLoader) {
            // 忽略 WRITE_REPLACE_METHOD ,和序列化相关
            if (WRITE_REPLACE_METHOD.equals(methodName)) {
                Object original;
                if (constructorArgTypes.isEmpty()) {
                    original = objectFactory.create(type);
                } else {
                    original = objectFactory.create(type, constructorArgTypes, constructorArgs);
                }
                PropertyCopier.copyBeanProperties(type, enhanced, original);
                if (lazyLoader.size() > 0) {
                    return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
                } else {
                    return original;
                }
            } else {
                if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
                    // 加载所有延迟加载的属性
                    if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                        lazyLoader.loadAll();
                    // 如果调用了 setting 方法,则不在使用延迟加载
                    } else if (PropertyNamer.isSetter(methodName)) {
                        final String property = PropertyNamer.methodToProperty(methodName);
                        lazyLoader.remove(property); // 移除
                    // 如果调用了 getting 方法,则执行延迟加载
                    } else if (PropertyNamer.isGetter(methodName)) {
                        final String property = PropertyNamer.methodToProperty(methodName);
                        if (lazyLoader.hasLoader(property)) {
                            lazyLoader.load(property);
                        }
                    }
                }
            }
        }
        // 继续执行原方法
        return methodProxy.invoke(enhanced, args);
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    } 
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容