Mybatis 懒加载使用及源码分析

2年前 (2022) 程序员胖胖胖虎阿
274 0 0

Mybatis 懒加载的使用

什么是懒加载?懒加载的意思就是在使用的时候才去加载,不使用不去加载,相反的就叫饥饿加载或者立即加载。懒加载在 Mybatis 中一般是存在与联合查询的情况,比如查询一个对象的同时连带查询相关的表对应的数据。在 Mybatis 中查询可以通过 ResultMap 设置查询对象返回一个集合属性,也就是说像这样的:

@Data
public class User implements Serializable {

    private int id;
    private int age;
    private String name;
    private List<Order> orderList;
}

这里的 orderList 就是一个集合,在 mapper.xml 中配置如下:

<resultMap id="userMap" type="mybatis.model.User">
    <id column="id" property="id"/>
    <result property="age" column="age"/>
    <result property="name" column="name"/>
    <collection property="orderList" ofType="mybatis.model.Order" column="id" select="findByUid"/>
</resultMap>

<select id="findByUid" resultType="mybatis.model.Order">
    select * from `order` where uid = #{id}
</select>

<select id="selectById" resultMap="userMap">
    select * from user where id = #{id}
</select>

可以看到这里查询 User 对象的时候还查询了 Order 列表,这个用户关联的订单信息。如果只是这样查询那么结果是饥饿加载:

@Test
public void testLazyLoad(){
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectById(1);
    System.out.println(user.getName());
}

输出结果,执行了两个 sql 语句查询,说明查询 User 的同时也查询了 Order

09:52:56.575 [main] INFO mybatis.plugins.MyPlugin - 对方法进行增强....
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 灵犀
Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.0
09:52:56.613 [main] INFO mybatis.plugins.MyPlugin - 对方法进行增强....
====>  Preparing: select * from `order` where uid = ? 
====> Parameters: 1(Integer)
<====    Columns: id, uid, order_name, price
<====        Row: 1, 1, 苹果, 8.00
<====        Row: 3, 1, 笔记本电脑, 8000.00
<====      Total: 2
<==      Total: 1
灵犀

Process finished with exit code 0

配置懒加载:

<resultMap id="userMap" type="mybatis.model.User">
    <id column="id" property="id"/>
    <result property="age" column="age"/>
    <result property="name" column="name"/>
    <collection property="orderList" ofType="mybatis.model.Order" column="id" select="findByUid" fetchType="lazy"/>
</resultMap>

这里的 collection 标签中的 fetchType 属性可以设置为 lazy 或者 eager ,默认就是 eager 饥饿加载,配置完之后执行:

09:56:22.649 [main] INFO mybatis.plugins.MyPlugin - 对方法进行增强....
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 灵犀
<==      Total: 1
灵犀

可以看到只执行了查询 user 的 sql 语句,而查询订单 order 的 sql 语句没有执行,只有在使用 orderList 这个属性的时候才会去执行 sql 查询:

@Test
public void testLazyLoad(){
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectById(1);
    System.out.println(user.getName());
    // 懒加载
    System.out.println(user.getOrderList());
}

输出结果:

09:58:02.681 [main] INFO mybatis.plugins.MyPlugin - 对方法进行增强....
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 灵犀
<==      Total: 1
灵犀
Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.0
09:58:02.746 [main] INFO mybatis.plugins.MyPlugin - 对方法进行增强....
==>  Preparing: select * from `order` where uid = ? 
==> Parameters: 1(Integer)
<==    Columns: id, uid, order_name, price
<==        Row: 1, 1, 苹果, 8.00
<==        Row: 3, 1, 笔记本电脑, 8000.00
<==      Total: 2
[Order(id=1, uid=1, orderName=苹果, price=8.00), Order(id=3, uid=1, orderName=笔记本电脑, price=8000.00)]

Process finished with exit code 0

可以看到执行查询订单的 sql 语句并且打印了订单信息
Mybatis 懒加载原理及源码解析

Mybatis 懒加载的原理要搞清楚的话,就需要去找到返回结果的时候看看 Mybatis 是如何封装的,找到 ResultSetHandler ,因为这个接口就是专门用于结果集封装的,默认实现为 DefaultResultSetHandler ,根据查询数据流程不难发现封装结果集的时候调用的是 handleResultSets 方法:

@Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    // 获取ResultSet的包装器
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    // 验证结果数量
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // 处理结果集
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

点击处理结果集的方法:

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        // 处理每行的数据
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }

点击处理每行的数据方法:

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    // 如果存在嵌套的结果集
    if (resultMap.hasNestedResultMaps()) {
      // 安全行约束检查,如果是嵌套查询需要关闭安全行约束条件
      ensureNoRowBounds();
      // 检查结果处理器是否符合嵌套查询约束
      checkResultHandler();
      // 执行嵌套查询结果集处理
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      // 简单的结果集分装处理
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }

由于我们写的这个结果是简单结果集,所以进入 handleRowValuesForSimpleResultMap :

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    ResultSet resultSet = rsw.getResultSet();
    skipRows(resultSet, rowBounds);
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      // 获取每行的值
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
  }

挑重点,直接进入获取每行值方法中:

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    // 创建结果值
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      if (shouldApplyAutomaticMappings(resultMap, false)) {
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
      }
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
  }

继续进入获取每行结果值的方法, createResultObject :

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    this.useConstructorMappings = false; // reset previous mapping result
    final List<Class<?>> constructorArgTypes = new ArrayList<>();
    final List<Object> constructorArgs = new ArrayList<>();
    // 创建结果对象 ,使用ObjectFactory 反射进行创建
    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()) {
          // 使用动态代理创建一个代理对象作为结果对象返回出去,默认使用javassist 进行创建
          resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
          break;
        }
      }
    }
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
    return resultObject;
  }

这里就先是通过反射创建出这个对象 resultObject ,然后遍历去检查这些属性是否是懒加载的,如果是那么就通过代理工厂去创建一个代理对象,由于这里创建的是一个返回对象,不是一个接口因此动态代理实现是通过 cglib 实现的, Mybatis 这里使用 javassist 包下的代理进行创建代理对象,代理工厂默认就是 JavassistProxyFactory :

static Object crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {

    ProxyFactory enhancer = new ProxyFactory();
    enhancer.setSuperclass(type);

    try {
      type.getDeclaredMethod(WRITE_REPLACE_METHOD);
      // ObjectOutputStream will call writeReplace of objects returned by writeReplace
      if (LogHolder.log.isDebugEnabled()) {
        LogHolder.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 });
    } 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;
  }

实际上这里也是通过反射进行创建,只是在外面封装成了 ProxyFactory 这个对象,当我们调用 getOrderList 方法的时候就会执行到 invoke 方法中,并且判断是否是延迟加载的,如果是那么就会执行 lazyLoader.load 方法执行延迟加载,也就是执行 sql 查询数据:

@Override
    public 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;
            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();
              } else if (PropertyNamer.isSetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                lazyLoader.remove(property);
                //判断方法是否是get方法
              } 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);
      }
    }
  }

load 方法就会执行真正的查询 sql 语句,将数据赋值给 User 对象,这样就完成了真正的懒加载操作,所以 Mybatis 的懒加载实际上就是利用动态代理将对象的参数封装进行了延迟加载,当需要时再去调用真正的查询操作并返回数据。

版权声明:程序员胖胖胖虎阿 发表于 2022年10月20日 上午7:32。
转载请注明:Mybatis 懒加载使用及源码分析 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...