mybatis collection解析以及和association的區別

2022-07-14 15:03:55

1.collection標籤

    說到mybatis的collection標籤,我們肯定不陌生,可以通過它解決一對多的對映問題,舉個例子一個使用者對應多個系統許可權,通過對使用者表和許可權表的關聯查詢我們可以得到好多條記錄,但是使用者資訊這部分在多條記錄中是重複的,只有許可權不同,我們需要把這多條許可權記錄對映到這個使用者之中,這個時候可以通過collection標籤/association標籤來解決(雖然assocation標籤一般是解決一對一問題的,但它實際上也能實現我們的需求,可以通過後面的原始碼看出來)

 

1.1 相關程式碼和執行結果

實體類和mapper程式碼
@Data
public class UserDO {

  private Integer userId;

  private String username;

  private String password;

  private String nickname;

  // 將使用者的許可權資訊對映到使用者中
  private List<PermitDO> permitDOList;

  public UserDO() {}

  public UserDO(@Param("userId") Integer userId, @Param("username") String username, @Param("password") String password, @Param("nickname") String nickname) {
    this.userId = userId;
    this.username = username;
    this.password = password;
    this.nickname = nickname;
  }
}



@Data
public class PermitDO {

  private Integer id;

  private String code;

  private String name;

  private NodeTypeEnum type;

  private Integer pid;
}


// mybatis程式碼 
public interface UserMapper {

  UserDO getByUserId(@Param("userId") Integer userId);

}

<mapper namespace="org.apache.ibatis.study.mapper.UserMapper">

  <cache readOnly="false" flushInterval="5000" size="100" blocking="false">

  </cache>

  <resultMap id="BaseMap" type="org.apache.ibatis.study.entity.UserDO" autoMapping="true">
     <!--  user_id列用<id>標籤,因為對一個使用者來說,user_id肯定是唯一的 -->
    <id column="user_id" jdbcType="INTEGER" property="userId" />
    <result column="username" jdbcType="VARCHAR" property="username" />
    <result column="password" jdbcType="VARCHAR" property="password" />
    <result column="nickname" jdbcType="VARCHAR" property="nickname"/>

    <!-- <collection> 對映多條許可權記錄 -->
    <collection property="permitDOList"
      resultMap="PermitBaseMap">

    </collection>
  </resultMap>

  <resultMap id="PermitBaseMap" type="org.apache.ibatis.study.entity.PermitDO">
    <id column="id" jdbcType="INTEGER" property="id"/>
    <result column="code" jdbcType="VARCHAR" property="code"/>
    <result column="name" jdbcType="VARCHAR" property="name"/>
    <result column="type" jdbcType="TINYINT" property="type"/>
    <result column="pid" jdbcType="INTEGER" property="pid"/>
  </resultMap>

  <resultMap id="BaseMap1" type="org.apache.ibatis.study.entity.UserDO" autoMapping="false">
    <constructor>
      <idArg column="user_id" name="userId" jdbcType="INTEGER" />
      <arg column="username" name="username" jdbcType="VARCHAR" />
      <arg column="password" name="password" jdbcType="VARCHAR" />
      <arg column="nickname" name="nickname" jdbcType="VARCHAR" />
    </constructor>
  </resultMap>

  <sql id="BaseFields">
    user_id, username, password, nickname
  </sql>

  <select id="getByUserId" resultMap="BaseMap" resultOrdered="true">
    select u.*, p.*
    from user u
    inner join user_permit up on u.user_id = up.user_id
    inner join permit p on up.permit_id = p.id
    <trim prefix="where" prefixOverrides="and | or">
      and u.user_id = #{userId, jdbcType=INTEGER}
    </trim>
  </select>

</mapper>

 

public class Test {

  public static void main(String[] args) throws IOException {

    try (InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) {
      // 構建session工廠 DefaultSqlSessionFactory
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
      SqlSession sqlSession = sqlSessionFactory.openSession();
      UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
      UserDO userDO = userMapper.getByUserId(1);
      System.out.println(userDO);
    }
  }

}

執行結果如下,可以看到許可權記錄對映到屬性permitDOList 的list列表了

 

1.2 collection部分原始碼解析

通過PreparedStatement查詢完之後得到ResultSet結果集,之後需要將結果集解析為java的pojo類中,下面通過原始碼簡單講下是如何解析的

  public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    // 是否有巢狀的resultMaps
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      // 無巢狀
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }

根據有無巢狀分成兩層邏輯,有巢狀resultMaps就是指<resultMap>標籤下有子標籤<collection>或<association>,分析下第一層

  private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    ResultSet resultSet = rsw.getResultSet();
    // 跳過offset行
    skipRows(resultSet, rowBounds);
    // 上一次獲取的資料
    Object rowValue = previousRowValue;
    // 已獲取記錄數量小於limit
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      // 鑑別器解析
      final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      // 建立快取key resultMapId + (columnName + columnValue)....
      final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
      // 部分物件(可能存在物件內容缺失未完全合併)
      Object partialObject = nestedResultObjects.get(rowKey);
      // issue #577 && #542
      // 關於resultOrdered的理解,舉例若查詢得到四條記錄a,a,b,a , 相同可以合併。
      // 那麼當resultOrdered=true時,最後可以得到三條記錄,第一條和第二條合併成一條、第三條單獨一條、第四條也是單獨一條記錄
      // resultOrdered=false時,最後可以得到兩條記錄,第一條、第二條和第四條會合併成一條,第三條單獨一條記錄
      // 另外儲存到resultHandler的時機也不一樣,resultOrdered=true是等遇到不可合併的記錄的時候才把之前已經合併的記錄儲存,
      // 而resultOrdered=false是直接儲存的後續有合併的記錄再處理新增到集合屬性中
      if (mappedStatement.isResultOrdered()) {
        // partialObject為null,說明這一條記錄不可與上一條記錄進行合併了,那麼清空nestedResultObjects防止之後出現有可合併的記錄的時候繼續合併
        // 然後將記錄儲存到resultHandler裡面
        if (partialObject == null && rowValue != null) {
          nestedResultObjects.clear();
          storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
        }
        rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
      } else {
        // 處理resultSet的當前這一條記錄
        rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
        if (partialObject == null) {
          // 將記錄儲存到resultHandler裡面
          storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
        }
      }
    }

這段程式碼主要是建立了一個快取key,主要是根據resultMapId和<id>標籤的column和對應的columvalue來建立的(若沒有<id>標籤,則會使用所有的<result>標籤的column和columnValue來建立),以此快取鍵來區分記錄是否可合併。nestedResultObjects是一個儲存結果的map,以快取鍵為key,實體類(本例中為UserDO)為value,若能以cacheKey取到值,則說明本條記錄可合併。

  private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {
    final String resultMapId = resultMap.getId();
    Object rowValue = partialObject;
    // rowValue不等於null時,說明此條記錄可合併
    if (rowValue != null) {
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      putAncestor(rowValue, resultMapId);
      applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
      ancestorObjects.remove(resultMapId);
    } else {
      final ResultLoaderMap lazyLoader = new ResultLoaderMap();
      // 建立result接收物件,本例中是UserDO物件
      rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
      if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        boolean foundValues = this.useConstructorMappings;
        // 是否將查詢出來的欄位全部對映 預設false
        if (shouldApplyAutomaticMappings(resultMap, true)) {
          foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
        }
        // 設定需要對映的屬性值,不管有巢狀ResultMap的
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
        // 存放第一條資料
        putAncestor(rowValue, resultMapId);
        // 處理有巢狀的resultMapping
        foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;
        ancestorObjects.remove(resultMapId);
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
      }
      // 將最終結果放入到nestedResultObjects中
      if (combinedKey != CacheKey.NULL_CACHE_KEY) {
        nestedResultObjects.put(combinedKey, rowValue);
      }
    }
    return rowValue;
  }

getRowValue方法主要是將ResultSet解析為實體類物件,applyPropertyMappings填充<id><result>標籤的實體屬性值

  private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject) {
    boolean foundValues = false;
    for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
      // 巢狀id
      final String nestedResultMapId = resultMapping.getNestedResultMapId();
      // resultMapping有巢狀的map才繼續 <association> <collection>
      if (nestedResultMapId != null && resultMapping.getResultSet() == null) {
        try {
          final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
          // 獲取巢狀(經過一次鑑權)的ResultMap
          final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
          if (resultMapping.getColumnPrefix() == null) {
            // try to fill circular reference only when columnPrefix
            // is not specified for the nested result map (issue #215)
            Object ancestorObject = ancestorObjects.get(nestedResultMapId);
            if (ancestorObject != null) {
              if (newObject) {
                linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
              }
              continue;
            }
          }
          // 構建巢狀map的key
          final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
          // 合併cacheKey
          final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
          // 嘗試獲取之前是否已經建立過
          Object rowValue = nestedResultObjects.get(combinedKey);
          boolean knownValue = rowValue != null;
          // 範例化集合屬性 list複製為空列表
          instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
          // 存在指定的非空列存在空值則返回false
          if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
            rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
            if (rowValue != null && !knownValue) {
              // 合併記錄,設定物件-association或將物件新增到集合屬性中-collection
              linkObjects(metaObject, resultMapping, rowValue);
              foundValues = true;
            }
          }
        } catch (SQLException e) {
          throw new ExecutorException("Error getting nested result map values for '" + resultMapping.getProperty() + "'.  Cause: " + e, e);
        }
      }
    }
    return foundValues;
  }

處理巢狀的結果對映,其實就是<collection><association>標籤。同時呼叫getRowValue方法根據<collection>指定的resultMap獲取實體物件(這裡是PermitDO物件),然後呼叫linkObjects方法將permitDO物件呼叫add方法新增到permitDOList中

private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
    final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
    // 屬性是集合進行新增 <collection>
    if (collectionProperty != null) {
      final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
      targetMetaObject.add(rowValue);
    } else {
      // 否則是物件 直接進行setter設定 <association>
      metaObject.setValue(resultMapping.getProperty(), rowValue);
    }
  }

最後就把能合併的記錄都合併在一起了,不同的許可權對映到permitDOList這個集合中了

 

1.3 <collection>和<association>的相同的和不同點

從上面的程式碼看來,關於<collection>和<association>標籤都屬於巢狀結果集了,處理邏輯也是基本相同的沒啥區分,換句話來說,把上面的<collection>替換成<association>標籤其實也能得到相同的結果,關鍵還是pojo類中javaType的屬性,若屬性為List則會建立空的list並將巢狀結果對映新增到list中(即使是一對一的那麼list中就只有一條記錄),若屬性為普通物件則直接進行setter設定。

從上面的圖中我們可以看到<collection>和<association>標籤屬性基本相同,<collection>比<association>多了一個ofType屬性,這個ofType屬性其實就是collection集合中單個元素的javaType屬性,<collection>的javaType屬性是繼承了Collection介面的list或set等java集合屬性。

另外在使用習慣上因為我們能確認表和表之間的關係是一對一還是一對多的,能夠確認pojo類中的屬性javaType是使用list還是普通物件,所以一般情況下一對一使用<association>標籤,一對多使用<collection>標籤,語意上更清晰更好理解。

 

最後

如果說的有問題歡迎提出指正討論,程式碼提交在gitee上,感興趣的同學可以下載看看