Mybatis 之 延迟加载

129 阅读4分钟

1、什么是延迟加载

问题
在开发过程中很多时候我们并不需要总是在加载⽤户信息时就⼀定要加载他的订单信息。此时就是我们所说的延迟加载。
举个栗⼦

  • 在⼀对多中,当我们有⼀个⽤户,它有个100个订单
    在查询⽤户的时候,要不要把关联的订单查出来?
    在查询订单的时候,要不要把关联的⽤户查出来?
  • 回答
    在查询⽤户时,⽤户下的订单应该是,什么时候⽤,什么时候查询。
    在查询订单时,订单所属的⽤户信息应该是随着订单⼀起查询出来。

延迟加载

就是在需要⽤到数据时才进⾏加载,不需要⽤到数据时就不加载数据。延迟加载也称懒加载。

  • 优点:
    先从单表查询,需要时再从关联表去关联查询,⼤⼤提⾼数据库性能,因为查询单表要⽐关联查询多张表
    速度要快。
  • 缺点:
    因为只有当需要⽤到数据时,才会进⾏数据库查询,这样在⼤批量数据查询时,因为查询⼯作也要消耗时
    间,所以可能造成⽤户等待时间变⻓,造成⽤户体验下降。
  • 在多表中:
    ⼀对多,多对多:通常情况下采⽤延迟加载
    ⼀对⼀(多对⼀):通常情况下采⽤⽴即加载
  • 注意:
    延迟加载是基于嵌套查询来实现的

2、实现

局部延迟加载
在association和collection标签中都有⼀个fetchType属性,通过修改它的值,可以修改局部的加载策略。

<!-- 开启⼀对多 延迟加载 -->
<resultMap id="userMap" type="user">
    <id column="id" property="id"></id>
    <result column="username" property="username"></result>
    <result column="password" property="password"></result>
    <result column="birthday" property="birthday"></result>
<!--
fetchType="lazy" 懒加载策略
fetchType="eager" ⽴即加载策略
-->
    <collection property="orderList" ofType="order" column="id"
        select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy">
    </collection>
</resultMap>
<select id="findAll" resultMap="userMap">
    SELECT * FROM `user`
</select>

全局延迟加载

在Mybatis的核⼼配置⽂件中可以使⽤setting标签修改全局的加载策略。

<settings>
    <!--开启全局延迟加载功能-->
    <setting name="lazyLoadingEnabled" value="true"/>
</settings>

注意
局部的加载策略的优先级高于全局的加载策略

<!-- 关闭⼀对⼀ 延迟加载 -->
<resultMap id="orderMap" type="order">
    <id column="id" property="id"></id>
    <result column="ordertime" property="ordertime"></result>
    <result column="total" property="total"></result>
<!--
fetchType="lazy" 懒加载策略
fetchType="eager" ⽴即加载策略
-->
    <association property="user" column="uid" javaType="user"
        select="com.lagou.dao.UserMapper.findById" fetchType="eager">
    </association>
</resultMap>
<select id="findAll" resultMap="orderMap">
    SELECT * from orders
</select>

3、延迟加载原理实现

它的原理是,使⽤ CGLIB 或 Javassist( 默认 ) 创建⽬标对象的代理对象。当调⽤代理对象的延迟加载属性的 getting ⽅法时,进⼊拦截器⽅法。⽐如调⽤ a.getB().getName() ⽅法,进⼊拦截器的
invoke(...) ⽅法,发现 a.getB() 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B
对象的 SQL ,把 B 查询上来,然后调⽤a.setB(b) ⽅法,于是 a 对象 b 属性就有值了,接着完
成a.getB().getName() ⽅法的调⽤。这就是延迟加载的基本原理

总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定⽅法,执⾏数据加载。

4、延迟加载原理(源码剖析)

MyBatis延迟加载主要使⽤:Javassist,Cglib实现,类图展示:

Setting 配置加载:

public class Configuration {
        /**
         * aggressiveLazyLoading:
         * 当开启时,任何⽅法的调⽤都会加载该对象的所有属性。否则,每个属性会按需加载(参考lazyLoadTriggerMethods).
         * 默认为true
         */
        protected boolean aggressiveLazyLoading;
        /**
         * 延迟加载触发⽅法
         */
        protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[]{"equals", "clone", "hashCode", "toString" }));
        /**
         * 是否开启延迟加载
         */
        protected boolean lazyLoadingEnabled = false;
 
        /**
         * 默认使⽤Javassist代理⼯⼚
         *
         * @param proxyFactory
         */
        public void setProxyFactory(ProxyFactory proxyFactory) {
            if (proxyFactory == null) {
                proxyFactory = new JavassistProxyFactory();
            }
            this.proxyFactory = proxyFactory;
        }
        //省略...
    }

延迟加载代理对象创建
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;
    }

默认采⽤javassistProxy进⾏代理对象的创建

注意事项
IDEA调试问题 当配置aggressiveLazyLoading=true,在使⽤IDEA进⾏调试的时候,如果断点打到
代理执⾏逻辑当中,你会发现延迟加载的代码永远都不能进⼊,总是会被提前执⾏。 主要产⽣的
原因在aggressiveLazyLoading,因为在调试的时候,IDEA的Debuger窗体中已经触发了延迟加载
对象的⽅法。