1. 程式人生 > >mybatis 原始碼分析(八)ResultSetHandler 詳解

mybatis 原始碼分析(八)ResultSetHandler 詳解

本篇部落格就是 myabtis 系列的最後一篇了,還剩 ResultSetHandler 沒有分析;作為整個 mybatis 最複雜最繁瑣的部分,我不打算按步驟一次詳解,因為裡面的主要內容就是圍繞 resultMap 按層次結構依次解析的,其中運用最多的就是反射,所以我這裡將圍繞延遲載入重點分析,另外本文使用的測試程式碼都是原始碼的測試案例;

一、ResultSetHandler 主體結構

public interface ResultSetHandler {
  // 負責結果集處理,完成對映返回結果物件
  <E> List<E> handleResultSets(Statement stmt) throws SQLException;
  // 負責遊標物件處理
  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
  // 負責儲存過程的輸出引數
  void handleOutputParameters(CallableStatement cs) throws SQLException;
}

以上就是 ResultSetHandler 的介面方法(mybatis 中只提供了唯一的實現類 DefaultResultSetHandler),在本篇部落格中將主要以 handleResultSets 結果集處理作為主線分析;

在分析之前首先要清楚 handleResultSets 方法的處理流程就是圍繞 resultMap 依次解析的,這裡先看一個比較複雜的 resultMap 對映:

<resultMap id="detailedBlogResultMap" type="Blog">
  <constructor>
    <idArg column="blog_id" javaType="int"/>
  </constructor>
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
  </association>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <association property="author" javaType="Author"/>
    <collection property="comments" ofType="Comment">
      <id property="id" column="comment_id"/>
    </collection>
    <discriminator javaType="int" column="draft">
      <case value="1" resultType="DraftPost"/>
    </discriminator>
  </collection>
  <association property="author" column="id" select="**.selectAuthorForBlog" fetchType="eager"/>
  <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="**.selectPostsForBlog" fetchType="lazy"/>
</resultMap>

當 mybatis 初始化完成後上面的配置都放到 MappedStatement.resultMaps 裡面,在解析的時候就是通過 resultMap.id 取到對應的 resultMap 然後逐次解析;

1. 巢狀查詢

這裡之所以說 ResultSetHandler 是整個 mybatis 裡面最複雜的,主要是巢狀查詢的解析(association 一對一,collection 一對多),值得注意的是這裡的巢狀查詢是有兩種方式的:

內部巢狀

<association property="author" javaType="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
  <result property="password" column="author_password"/>
</association>

<collection property="posts" ofType="Post">
  <id property="id" column="post_id"/>
  <result property="subject" column="post_subject"/>
  <association property="author" javaType="Author"/>
  <collection property="comments" ofType="Comment">
    <id property="id" column="comment_id"/>
  </collection>
  <discriminator javaType="int" column="draft">
    <case value="1" resultType="DraftPost"/>
  </discriminator>
</collection>

像這種巢狀查詢是直接在同一個 resultMap 依次對映對應結果的,使用的是 SQL 多表連線,例如:

<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
  select
       B.id as blog_id,
       B.title as blog_title,
       B.author_id as blog_author_id,
       A.id as author_id,
       A.username as author_username,
       ...
       P.id as post_id,
       P.blog_id as post_blog_id,
       ...
       C.id as comment_id,
       C.post_id as comment_post_id,
       ...
       T.id as tag_id,
       T.name as tag_name
  from Blog B
       left outer join Author A on B.author_id = A.id
       left outer join Post P on B.id = P.blog_id
       left outer join Comment C on P.id = C.post_id
       left outer join Post_Tag PT on PT.post_id = P.id
       left outer join Tag T on PT.tag_id = T.id
  where B.id = #{id}
</select>

這裡還有一種分離的內部巢狀:

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
</resultMap>

<resultMap id="authorResult" type="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
  <result property="password" column="author_password"/>
  <result property="email" column="author_email"/>
  <result property="bio" column="author_bio"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
  select
    B.id            as blog_id,
    B.title         as blog_title,
    B.author_id     as blog_author_id,
    A.id            as author_id,
    A.username      as author_username,
    A.password      as author_password,
    A.email         as author_email,
    A.bio           as author_bio
  from Blog B left outer join Author A on B.author_id = A.id
  where B.id = #{id}
</select>

這中寫法只是將 association、collection 部分分離出來,其實質都是一樣的,都是一條多表連線的 SQL;

外部巢狀

還有另外一種是將多表連線的 SQL 拆分,每個屬性單獨發一條 SQL:

<resultMap id="blogResult" type="Blog">
  <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectPostsForBlog" resultType="Post">
  SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>

像這樣在 association、collection 中使用 select 屬性指定外部 SQL,其查詢結果也是發兩條 SQL,這裡之所以沒有詳細寫出每個屬性的對映,是因為指定了 type 和 ofType,並開啟的自動對映,mybatis 在執行的時候使用反射推斷出來的;

這裡的兩種巢狀查詢在初始化的時候就進行了單獨的區分:

// org.apache.ibatis.builder.xml.XMLMapperBuilder
String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections.emptyList(), resultType));

// org.apache.ibatis.submitted.nestedresulthandler.Mapper.mapper_resultMap[personResult]_collection[items]
private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) throws Exception {
  if ("association".equals(context.getName())
      || "collection".equals(context.getName())
      || "case".equals(context.getName())) {
    if (context.getStringAttribute("select") == null) {
      validateCollection(context, enclosingType);
      ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
      return resultMap.getId();
    }
  }
  return null;
}

// org.apache.ibatis.mapping.ResultMap
// resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);

這裡程式碼程式碼比較多所以只放了關鍵程式碼,其最終結果是內部巢狀由 ResultMap.hasNestedResultMaps 標識;

// org.apache.ibatis.builder.xml.XMLMapperBuilder
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
  ...
  String nestedSelect = context.getStringAttribute("select");
  String nestedResultMap = context.getStringAttribute("resultMap",
      processNestedResultMappings(context, Collections.emptyList(), resultType));
  ...
}

外部查詢的最終結果是由 ResultMapping.nestedQueryId 儲存的,之所以這裡強調這些是因為在 ResultSetHandler 解析的時候是分了內外部巢狀兩種大的情況的;

2. 多結果集

此外分析之前首先還要知道 CallableStatement 呼叫儲存過程的時候,會有多結果集的情況,例如:

create procedure sptest.getnamesanditemsbyid(in nameId integer)
modifies sql data
dynamic result sets 2
BEGIN ATOMIC
  declare cur1 cursor for select * from sptest.names where id = nameId;
  declare cur2 cursor for select * from sptest.items where name_id in (select id from sptest.names where id = nameId);
  open cur1;
  open cur2;
END
<resultMap type="org.apache.ibatis.submitted.sptests.Name" id="nameResultLinkedNoMatchingInfo">
  <result column="ID" property="id"/>
  <result column="FIRST_NAME" property="firstName"/>
  <result column="LAST_NAME" property="lastName"/>
  <collection property="items" resultSet="items" resultMap="itemResult"/>
</resultMap>

<select id="getNamesAndItemsLinkedById" statementType="CALLABLE" resultSets="names,items" resultMap="nameResultLinkedNoMatchingInfo">
  {call sptest.getnamesanditemsbyid(#{id,jdbcType=INTEGER,mode=IN})}
</select>

2. 整體流程

上圖就是 ResultSetHandler.handleResultSet 的主要流程,這裡只保留了重要的部分:

  • 內外部巢狀查詢的分支;
  • 外部巢狀查詢與一級快取;
  • 外部巢狀查詢的延遲載入,主要是代理物件、ResultLoader、ResultLoaderMap三個物件;

其餘的部分這裡就不再詳細分析了,一下將主要講解外部巢狀查詢的延遲載入;

二、cglib 和 javassisit 動態代理

在講解延遲載入之前,需要首先簡單瞭解一下動態代理,因為普通的 JavaBean 物件一般都沒有實現介面,所以不能使用 java.lang.reflect.Proxy,在 mybatis 中提供了另外兩種動態代理 cglib 和 javassisit;

1. cglib

public class Car {
  String name;
  
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
}

@Test
public void test() {
  Enhancer enhancer = new Enhancer();
  enhancer.setSuperclass(Car.class);
  enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
    System.out.println("代理開始");
    Object object = proxy.invokeSuper(obj, args);
    System.out.println("result: " + object);
    System.out.println("代理結束");
    return object;
  });

  Car car = (Car) enhancer.create();
  car.setName("Test");
  car.getName();
}

列印:

代理開始
result: null
代理結束
代理開始
result: Test
代理結束

2. javassist

public class Car {
  String name;
  
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
}

@Test
public void test() throws IllegalAccessException, InstantiationException {
  ProxyFactory proxyFactory = new ProxyFactory();
  proxyFactory.setSuperclass(Car.class);
  // 設定攔截目標
  proxyFactory.setFilter(m -> m.getName().startsWith("get") || m.getName().startsWith("set"));
  proxyFactory.setHandler((self, thisMethod, proceed, arg) -> {
    System.out.println("代理開始");
    Object object = proceed.invoke(self, arg);
    System.out.println("result: " + object);
    System.out.println("代理結束");
    return object;
  });

  Class clazz = proxyFactory.createClass();
  Car car = (Car) clazz.newInstance();
  car.setName("Test");
  car.getName();
}

列印:

代理開始
result: null
代理結束
代理開始
result: Test
代理結束

三、延遲載入

通過上面的講解大家應該清楚只有外部巢狀查詢才有延遲載入功能;此外和延遲載入相關的配置:

  • proxyFactory:(CGLIB | JAVASSIST-預設)指定 mybatis 延遲載入的代理工具;
  • lazyLoadingEnabled:(true | false-預設)延遲載入的全域性開關。可使用 association、collection 的 fetchType (lazy|eager)屬性覆蓋;
  • aggressiveLazyLoading:(false| true-預設)當開啟時,任何方法的呼叫都會載入該物件的所有屬性。 否則每個屬性會按需載入;

1. demo

<setting name="proxyFactory" value="JAVASSIST"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<resultMap id="user" type="org.apache.ibatis.submitted.lazy_properties.User">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
</resultMap>

<resultMap id="userWithLazyProperties" type="org.apache.ibatis.submitted.lazy_properties.User" extends="user">
  <association property="lazy1" column="id" select="getLazy1" fetchType="lazy"/>
  <association property="lazy2" column="id" select="getLazy2" fetchType="eager"/>
  <collection property="lazy3" column="id" select="getLazy3" fetchType="lazy"/>
</resultMap>

<select id="getUser" resultMap="userWithLazyProperties">
  select * from users where id = #{id}
</select>
public class User {
  private Integer id;
  private String name;
  private User lazy1;
  private User lazy2;
  private List<User> lazy3;
  ...
 }

@Test
void test() {
  try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    Mapper mapper = sqlSession.getMapper(Mapper.class);
    User user = mapper.getUser(1);
    System.out.println("----getLazy1: " + user.getLazy1());
    System.out.println("----getLazy2: " + user.getLazy2());
    System.out.println("----getLazy3: " + user.getLazy3());
  }
}

列印:

DEBUG [main] - ==> Preparing: select * from users where id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - ====> Preparing: select 12 id, 'lazy2' name from (values(0))
DEBUG [main] - ====> Parameters:
DEBUG [main] - <==== Total: 1
DEBUG [main] - <== Total: 1
DEBUG [main] - ==> Preparing: select 11 id, 'lazy1' name from (values(0))
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 1
----getLazy1: User{id=11, name='lazy1'}
----getLazy2: User{id=12, name='lazy2'}
DEBUG [main] - ==> Preparing: select 13 id, 'lazy3' name from (values(0))
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 1
----getLazy3: [User{id=13, name='lazy3'}]

從列印的順序可以看出當 mapper.getUser(1) 的時候,就已經獲取了 user 和 lazy2,而 lazy1 和 lazy3 則是在 get 的時候才載入;這裡在看一下 aggressiveLazyLoading = true 的效果:

列印:

DEBUG [main] - ==> Preparing: select * from users where id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - ====> Preparing: select 12 id, 'lazy2' name from (values(0))
DEBUG [main] - ====> Parameters:
DEBUG [main] - <==== Total: 1
DEBUG [main] - ====> Preparing: select 11 id, 'lazy1' name from (values(0))
DEBUG [main] - ====> Parameters:
DEBUG [main] - <==== Total: 1
DEBUG [main] - ====> Preparing: select 13 id, 'lazy3' name from (values(0))
DEBUG [main] - ====> Parameters:
DEBUG [main] - <==== Total: 1
DEBUG [main] - <== Total: 1
----getLazy1: User{id=11, name='lazy1'}
----getLazy2: User{id=12, name='lazy2'}
----getLazy3: [User{id=13, name='lazy3'}]

這裡也能看到首先是獲取 user 和 lazy2,然後在 user.getLazy1() 的時候同時載入了 lazy1 和 lazy3;

2. 建立代理

在上面已經講過了,在使用延遲載入的時候:

  • 首先判斷是否有延遲載入屬性,有就使用代理包裝結果集物件;
  • 然後判斷一級快取中時候有對應的外部巢狀,有就取快取;如果沒有就將外部巢狀包裝為 ResultLoader 物件;
  • 然後判斷外部巢狀是否需要延遲載入,如果是就將 ResultLoader 加入到 ResultLoaderMap 中,如果不需要就直接載入 resultLoader.loadResult();

建立代理:首先獲取代理工廠,然後建立代理類;

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<>();
  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) {
      if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
        resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
        break;
      }
    }
  }
  this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty();
  return resultObject;
}

3. 代理工廠

這裡 CglibProxyFactory 和 JavassistProxyFactory 的流程都是一樣的,所以我們就以 CglibProxyFactory 為例進行簡單分析:

crateProxy:

static Object crateProxy(Class<?> type, Callback callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
  Enhancer enhancer = new Enhancer();
  enhancer.setCallback(callback);
  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;
  if (constructorArgTypes.isEmpty()) {
    enhanced = enhancer.create();
  } else {
    Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
    Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
    enhanced = enhancer.create(typesArray, valuesArray);
  }
  return enhanced;
}

這裡建立大致和上面給出的 demo 差不多,都是指定父類,設定回撥;接下來我們繼續看攔截的具體內容:

private final ResultLoaderMap lazyLoader;
public Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
  final String methodName = method.getName();
  try {
    // 鎖定 ResultLoaderMap 物件
    synchronized (lazyLoader) {
      // 建立代理的時候設定的 WriteReplaceInterface 介面
      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 CglibSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
        } else {
          return original;
        }
      // 真正延遲載入的邏輯處理
      } else {
        // ResultLoaderMap 數量大於 0,就表示還有待載入的屬性
        if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
          // aggressive = true,或者 equals、clone、hashCode、toString 之一,就載入全部方法
          if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
            lazyLoader.loadAll();
          // 呼叫某屬性的 set 方法時,表示不需要從資料庫再載入了,所以將其移除
          } else if (PropertyNamer.isSetter(methodName)) {
            final String property = PropertyNamer.methodToProperty(methodName);
            lazyLoader.remove(property);
          // 載入單個屬性
          } else if (PropertyNamer.isGetter(methodName)) {
            final String property = PropertyNamer.methodToProperty(methodName);
            if (lazyLoader.hasLoader(property)) {
              lazyLoader.load(property);
            }
          }
        }
      }
    }
    return methodProxy.invokeSuper(enhanced, args);
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

4. 延遲載入屬性載入

// org.apache.ibatis.executor.loader.ResultLoaderMap
public boolean load(String property) throws SQLException {
  // 先刪除 key,防止第二次查詢資料庫
  LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
  if (pair != null) {
    // 查資料庫
    pair.load();
    return true;
  }
  return false;
}

public void load(final Object userObject) throws SQLException {
  if (this.metaResultObject == null || this.resultLoader == null) {
    ...
    this.metaResultObject = config.newMetaObject(userObject);
    this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
            metaResultObject.getSetterType(this.property), null, null);
  }

  /* We are using a new executor because we may be (and likely are) on a new thread
   * and executors aren't thread safe. (Is this sufficient?)
   *
   * A better approach would be making executors thread safe. */
  if (this.serializationCheck == null) {
    final ResultLoader old = this.resultLoader;
    this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
                                         old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
  }
  // 查詢資料庫,並反射設定屬性
  this.metaResultObject.setValue(property, this.resultLoader.loadResult());
}
// org.apache.ibatis.executor.loader.ResultLoader
public Object loadResult() throws SQLException {
  // 查詢結果
  List<Object> list = selectList();
  // 轉換結果型別
  resultObject = resultExtractor.extractObjectFromList(list, targetType);
  return resultObject;
}

// 這裡又是從 Executor 出發,再查資料庫了
private <E> List<E> selectList() throws SQLException {
  Executor localExecutor = executor;
  if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
    localExecutor = newExecutor();
  }
  try {
    return localExecutor.<E> query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
  } finally {
    if (localExecutor != executor) {
      localExecutor.close(false);
    }
  }
}

以上就是延遲載入的全部流程了,

5. 延遲載入與一級快取

上面我們將了當一級快取中有外部巢狀查詢快取的時候,會直接取快取,而不是延遲載入:

private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
    throws SQLException {
  final String nestedQueryId = propertyMapping.getNestedQueryId();
  final String property = propertyMapping.getProperty();
  final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
  final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
  final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
  Object value = null;
  if (nestedQueryParameterObject != null) {
    final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
    final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
    final Class<?> targetType = propertyMapping.getJavaType();
    // 判斷一級快取
    if (executor.isCached(nestedQuery, key)) {
      executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
      value = DEFERRED;
    } else {
      final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
      if (propertyMapping.isLazy()) {
        lazyLoader.addLoader(property, metaResultObject, resultLoader);
        value = DEFERRED;
      } else {
        value = resultLoader.loadResult();
      }
    }
  }
  return value;
}

下面我們就實驗一下:

<setting name="proxyFactory" value="JAVASSIST"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<resultMap id="FatherMap" type="Father">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
  <association property="grandFather" column="grand_father_id"
               select="org.apache.ibatis.submitted.lazyload_common_property.GrandFatherMapper.selectById"
               fetchType="lazy"/>
</resultMap>

<select id="selectById" resultMap="FatherMap" parameterType="int">
    SELECT id, name, grand_father_id FROM Father WHERE id = #{id}
</select>
<resultMap id="GrandFatherMap" type="GrandFather">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
</resultMap>

<select id="selectById" resultMap="GrandFatherMap" parameterType="int">
    SELECT id, name FROM GrandFather WHERE id = #{id}
</select>
@Test
void test1() {
  try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    GrandFatherMapper grandFatherMapper = sqlSession.getMapper(GrandFatherMapper.class);
    FatherMapper fatherMapper = sqlSession.getMapper(FatherMapper.class);

    GrandFather grandFather = grandFatherMapper.selectById(1);
    System.out.println("----- get grandFather: " + grandFather);

    Father father = fatherMapper.selectById(1);
    System.out.println("----- get father: " + father.getName());
    System.out.println("----- get father.grandFather: " + father.getGrandFather());
  }
}

列印:

DEBUG [main] - ==> Preparing: SELECT id, name FROM GrandFather WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
----- get grandFather: GrandFather{id=1, name='John Smith sen'}
DEBUG [main] - ==> Preparing: SELECT id, name, grand_father_id FROM Father WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
----- get father: John Smith
----- get father.grandFather: GrandFather{id=1, name='John Smith sen'}

這裡我們首先獲取了一次 GrandFather,保證一級快取中有,然後獲取 Father,延遲載入 GrandFather;從上面的結果可以看到,確實延遲載入是從一級快取中取的;

6. 延遲載入與二級快取

上面我們講過了外部巢狀查詢的時候是從 Executor 開始的,那麼必然有一級快取和二級快取;這裡先說結論巢狀查詢使用二級快取一定要在同一個 namespace 裡面,否則會出現髒讀現象;下面舉例說明:

<setting name="proxyFactory" value="JAVASSIST"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="localCacheScope" value="STATEMENT"/>
<setting name="cacheEnabled" value="true"/>
// org/apache/ibatis/submitted/lazyload_common_property/FatherMapper.xml
<mapper namespace="org.apache.ibatis.submitted.lazyload_common_property.FatherMapper">
  <cache/>

  <resultMap id="FatherMap" type="Father">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <association property="grandFather" column="grand_father_id"
                 select="org.apache.ibatis.submitted.lazyload_common_property.GrandFatherMapper.selectById"
                 fetchType="lazy"/>
  </resultMap>

  <select id="selectById" resultMap="FatherMap" parameterType="int">
        SELECT id, name, grand_father_id FROM Father WHERE id = #{id}
  </select>

  <update id="updateById" flushCache="true">
    update Father set name = #{name} where id = #{id}
  </update>
</mapper>
// org/apache/ibatis/submitted/lazyload_common_property/GrandFatherMapper.xml
<mapper namespace="org.apache.ibatis.submitted.lazyload_common_property.GrandFatherMapper">
  <cache/>

  <resultMap id="GrandFatherMap" type="GrandFather">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
  </resultMap>

  <select id="selectById" resultMap="GrandFatherMap" parameterType="int">
    SELECT id, name FROM GrandFather WHERE id = #{id}
  </select>

  <update id="updateById" flushCache="true">
    update GrandFather set name = #{name} where id = #{id}
  </update>
</mapper>
@Test
void test2() {
  try (SqlSession sqlSession1 = sqlSessionFactory.openSession();
       SqlSession sqlSession2 = sqlSessionFactory.openSession();
  ) {
    GrandFatherMapper grandFatherMapper1 = sqlSession1.getMapper(GrandFatherMapper.class);
    GrandFatherMapper grandFatherMapper2 = sqlSession2.getMapper(GrandFatherMapper.class);
    FatherMapper fatherMapper1 = sqlSession1.getMapper(FatherMapper.class);
    FatherMapper fatherMapper2 = sqlSession2.getMapper(FatherMapper.class);

    Father father1 = fatherMapper1.selectById(1);
    System.out.println("----- session1 get father(put cache): " + father1);
    sqlSession1.commit();

    Father father2 = fatherMapper2.selectById(1);
    System.out.println("----- session2 get father(get cache): " + father2);

    // 測試重點
    // fatherMapper1.updateById(1, "TestName");
    grandFatherMapper1.updateById(1, "TestName");
    sqlSession1.commit();
    System.out.println("----- session1 update(put cache)");

    Father father3 = fatherMapper2.selectById(1);
    System.out.println("----- session2 get father(get cache): " + father3);
  }
}

測試流程:

  • 首先 session1 查詢並提交二級快取
  • 然後 session2 查詢檢查二級快取是否生效
  • 然後 session1 修改快取,並提交
  • 最後 session2 再查查詢,看是否檢查到快取的修改

列印:

DEBUG [main] - ==> Preparing: SELECT id, name, grand_father_id FROM Father WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.submitted.lazyload_common_property.GrandFatherMapper]: 0.0
DEBUG [main] - ==> Preparing: SELECT id, name FROM GrandFather WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
----- session1 get father(put cache): Father{id=1, name='John Smith', grandFather=GrandFather{id=1, name='John Smith sen'}}
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.submitted.lazyload_common_property.FatherMapper]: 0.5
----- session2 get father(get cache): Father{id=1, name='John Smith', grandFather=GrandFather{id=1, name='John Smith sen'}}
DEBUG [main] - ==> Preparing: update GrandFather set name = ? where id = ?
DEBUG [main] - ==> Parameters: TestName(String), 1(Integer)
DEBUG [main] - <== Updates: 1
DEBUG [main] - Committing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@2f01783a]
----- session1 update(put cache)
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.submitted.lazyload_common_property.FatherMapper]: 0.6666666666666666
----- session2 get father(get cache): Father{id=1, name='John Smith', grandFather=GrandFather{id=1, name='John Smith sen'}}

注意看這裡二級快取生效了,但是出現了髒讀:

然後我們將上面的註釋開啟:

DEBUG [main] - ==> Preparing: SELECT id, name, grand_father_id FROM Father WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.submitted.lazyload_common_property.GrandFatherMapper]: 0.0
DEBUG [main] - ==> Preparing: SELECT id, name FROM GrandFather WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
----- session1 get father(put cache): Father{id=1, name='John Smith', grandFather=GrandFather{id=1, name='John Smith sen'}}
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.submitted.lazyload_common_property.FatherMapper]: 0.5
----- session2 get father(get cache): Father{id=1, name='John Smith', grandFather=GrandFather{id=1, name='John Smith sen'}}
DEBUG [main] - ==> Preparing: update Father set name = ? where id = ?
DEBUG [main] - ==> Parameters: TestName(String), 1(Integer)
DEBUG [main] - <== Updates: 1
DEBUG [main] - ==> Preparing: update GrandFather set name = ? where id = ?
DEBUG [main] - ==> Parameters: TestName(String), 1(Integer)
DEBUG [main] - <== Updates: 1
DEBUG [main] - Committing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@2f01783a]
----- session1 update(put cache)
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.submitted.lazyload_common_property.FatherMapper]: 0.3333333333333333
DEBUG [main] - ==> Preparing: SELECT id, name, grand_father_id FROM Father WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.submitted.lazyload_common_property.GrandFatherMapper]: 0.0
DEBUG [main] - ==> Preparing: SELECT id, name FROM GrandFather WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
----- session2 get father(get cache): Father{id=1, name='TestName', grandFather=GrandFather{id=1, name='TestName'}}

這次發現髒讀消失了??其原因就是第一次之修改了 GrandFather,雖然 Father 中有 GrandFather 屬性,但是重新整理快取的時候並不會重新整理 Father,所以出現的髒讀;其解決辦法就是使用 將快取放在同一個名稱空間內;

這裡再提醒一下本文中使用的測試案例都能在 mybatis 原始碼的單元測試用找到