idea 测试 Springboot mybatis 懒加载

发布时间:2021-04-17作者:laosun阅读(1541)

Springboot mybatis 懒加载测试

    使用idea测试mybatis懒加载果真有坑


    为了映射对象中复杂的关联对象, 我们在 ResultMap 中可以配置 association 和 collection. 这两者的实现方式可以通过


    再请求一次 select (Nested Select)

    通过 join 将所有的属性读取出来 (Nested Results).

    但有时候我们并不会使用到对象中所有的属性, 所以这些额外的从数据库拉来的数据就浪费了. 对于通过 select 实现的方式我们可以使用懒加载来提升效率。

    配置

    lazyLoadingEnabled

    默认为 false, 也就是不使用懒加载. 所以如果 association 和 collection 使用了 select, 那么 MyBatis 会一次性执行所有的查询. 如果 accociation 和 collection 中的 fetchType 指定为 lazy, 那么即使 lazyLoadingEnabled 为 false, MyBatis 也会使用懒加载.

    Java 配置中 @One 和 @Many 的 fetchType 支持三个值: LAZYEAGERDEFAULT, 其中 DEFAULT 的意思是跟随全局设置即 lazyLoadingEnabled.

    aggressiveLazyLoading

    默认为 true, 也就是说当你开启了懒加载之后, 只要调用返回的对象中的 任何一个方法, 那么 MyBatis 就会加载所有的懒加载的属性, 即执行你配置的 select 语句.

    lazyLoadTriggerMethods

    默认值为 equals,clone,hashCode,toString, 当你调用这几个方法时, MyBatis 会加载所有懒加载的属性.

    proxyFactory

    默认为 JAVASSIST (MyBatis 3.3 or above). 懒加载是通过字节码增强实现的, 3.3 以前是通过 cglib 实现的, 3.3 之后包括 3.3 是使用 javassist 实现的.

    源码分析

    前面提到了懒加载是通过字节码增强实现的, 所以 MyBatis 会动态代理你的类, 然后根据调用的方法名来判断是否需要加载属性.

    相关类的实现有两个, 分别对应 javassist 和 cglib 的版本:

    • javassist: org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory

    • cglib: org.apache.ibatis.executor.loader.cglib.CglibProxyFactory

    两个类在方法拦截时的处理逻辑是一样的, 我们挑其中一个来看 (javassist 的):

    @Overridepublic 
    Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
      final String methodName = method.getName();
      try {
        synchronized (lazyLoader) {
          if (WRITE_REPLACE_METHOD.equals(methodName)) { // 这段不懂是处理什么情况的, 没细看
            Object original = null;
            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)) { 
                // 如果开启了 aggressive 或者调用的是 lazyLoadTriggerMethods 中设置的方法, 则加载所有属性
                lazyLoader.loadAll();
              } else if (PropertyNamer.isProperty(methodName)) { // 判断方法是否是以 get, set, is 开头
                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);
      }}

    注意事项

    aggressiveLazyLoading

    项目中开启了懒加载后, 准备测试下是否真的启用了懒加载, 于是打印日志看了下对应的属性在调用相应的方法前是否是 null. 结果发现属性每次都被加载了, 以为 MyBatis 能拦截属性的直接访问或者生成代理类的时候会分析相应字节码, 如果发现字节码中有属性的访问就在访问该方法时加载属性, 查了下 cglib 和 javassist 文档感觉并没有相应的功能啊.

    最后发现原来是因为 aggressiveLazyLoading 默认是开启的, 因为我访问了对象的其他方法所以属性被加载了.

    IDEA

    千万不要用下断点的方式查看对应的属性有没有被加载, 可能 是因为 IDEA 在 debug 的时候会调用 lazyLoadTriggerMethods 中的方法的, 所以导致属性被加载.

    Jackson

    MyBatis 生成的代理类会多出一个 handler 的属性, 从而导致 Jackson 序列化失败, 可以通过在类上添加注解来忽略该属性:

    @JsonIgnoreProperties("handler")
    public class MyDO {}

    以上文章转载自 https://yoncise.com/2016/11/05/MyBatis-%E8%AE%B0%E5%BD%95%E4%BA%8C-lazy-loading/



    具体的测试方式,请看以下mybatis xml:

    第一种方式:直接在mybatis 中使用 fetchType="lazy"

    第二种方式,配置全局懒加载:

    #开启mybatis延迟加载
    #全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。
    mybatis.configuration.lazy-loading-enabled=true
    mybatis-plus.configuration.lazy-loading-enabled=true
    #当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。
    mybatis.configuration.aggressive-lazy-loading=false
    mybatis-plus.configuration.aggressive-lazy-loading=false
    #懒加载情况下报“No serializer found for class org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory$Enha“
    #和@JsonIgnoreProperties("handler")类似
    spring.jackson.serialization.fail-on-empty-beans=false


    测试代码(以下代码和配置的名称有修改,未经测试):

    #MyBatis 生成的代理类会多出一个 handler 的属性, 从而导致 Jackson 序列化失败, 可以通过在类上添加注解来忽略该属性
    @JsonIgnoreProperties("handler")
    public class Users implements Serializable {
    
        private Integer id;
        private String name;
        private Orders orders;//假如一个用户仅有一笔订单
        
        set get...
    }
    public class Orders implements Serializable {
    
        private Integer userId;
        private String orderNo;
        
        set get...
    }
    <!-- 懒加载 -->
    <resultMap id="LAZY_TEST" type="com.xxx.entity.Users" extends="BaseResultMap">
        <collection property="orders" ofType="com.xxx.entity.Orders" column="id" select="twoSelect" fetchType="lazy">
    
        </collection>
    </resultMap>
    <!-- 根据主键id查找用户 -->
    <select id="findByIdOfLazy" parameterType="java.lang.Integer" resultMap="LAZY_TEST">
        select * from t_users where id = #{id}
    </select>
    <!-- 根据用户id查找订单 -->
    <select id="twoSelect" parameterType="java.lang.Integer" resultType="com.xxx.entity.Orders">
        select `user_id`, `order_no` from t_orders where user_id = #{id}
    </select>
    @RequestMapping(value = "/testLazy")
    public Users testLazy(Integer id) {
        Users users = usersMapper.findByIdOfLazy(id);
        logger.info("------------");#在idea中打断点会导致order属性也加载,所以这里输出一些日志来区分
        users.getOrders();
        return users;
    }

    输出日志:

    Preparing: select * from t_users where id = ? 
    Parameters: 1(Integer)
    Total: 1
    ------------
    Preparing: select `user_id`, `order_no` from t_orders where id = ? 
    Parameters: 1(Integer)
    Total: 1


1 +1

版权声明

 Java  源码  springboot  mybatis

 请文明留言

0 条评论