Mybatis 懒加载使用及源码分析

Mybatis 懒加载使用及源码分析

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
灵犀

可以看到只执行了查询usersql语句,而查询订单ordersql语句没有执行,只有在使用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的懒加载实际上就是利用动态代理将对象的参数封装进行了延迟加载,当需要时再去调用真正的查询操作并返回数据。

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » Mybatis 懒加载使用及源码分析