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的懶載入實際上就是利用動態代理將對象的參數封裝進行了延遲載入,當需要時再去調用真正的查詢操作並返回數據。

Tags: