【Mybatis系列】從原始碼角度理解Mybatis欄位對映-AS&ResultMap

凱倫說_美團點評發表於2019-03-04

前言

考慮到在Select時使用AS和方案一其實沒什麼差別,在介紹ResultMap之前,順便帶過一下。

方案二-Select …. AS

當我們的資料庫列名和物件欄位之間不是駝峰式命名的關係,我們可以在Select時使用AS,使得列名和物件名匹配上。
對映檔案中是本次會執行的sql,我們會查出id,city_id,city_name,city_en_name。 按照開啟的駝峰式命名開關,我們會對應到物件的id,cityId,cityName,cityEnName欄位。

<select id="selectCity" resultType="po.CityPO">
    select id,city_id,city_name,city_en_name from SU_City where id = #{id}
</select>複製程式碼

不過在這次,我們對PO做了小小的改動,把cityEnName改成了cityEnglishName。

public class CityPO {
    Integer id;
    Long cityId;
    String cityName;
    String cityEnglishName;   // 由cityEnName改成了cityEnglishName複製程式碼

由於找不到匹配的列,cityEnlishName肯定沒法被反射賦值,要為Null了。

CityPO{id=2, cityId=2, cityName=`北京`, cityEnglishName=`null`}複製程式碼

解決辦法: 在Select欄位的時候使用AS,下面是改動後的對映檔案。

<select id="selectCity" resultType="po.CityPO">
        select id,
        city_id,
        city_name,
        city_en_name AS cityEnglishName
        from SU_City
        where id = #{id}
</select>複製程式碼

改動後執行得到的結果如下。

CityPO{id=2, cityId=2, cityName=`北京`, cityEnglishName=`beijing`}複製程式碼

那麼我們來看看它是如何生效的,主要的程式碼在哪裡。在昨天我們第一個介紹的函式handleRowValues中傳入了引數rsw,它是對ResultSet的一個包裝,在這個包裝裡,完成了具體使用哪個名字作為資料庫的列名。

final ResultSetWrapper rsw = new ResultSetWrapper(rs, configuration);
handleRowValues(rsw, resultMap, resultHandler, new RowBounds(), null);複製程式碼

在這個建構函式當中,我們會獲取資料庫的列名,AS為什麼可以生效,具體就在下面這段程式碼。

super();
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.resultSet = rs;
final ResultSetMetaData metaData = rs.getMetaData();
final int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
      // 在這裡
      columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
      jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
      classNames.add(metaData.getColumnClassName(i));
}複製程式碼

在新增列名時,會從配置中獲取是否使用類標籤,isUseColumnLabel,預設為true。根據Javadoc,這個ColumnLabel就是AS後的那個名字,如果沒有AS的話,就是獲取的原生的欄位名。

    /**
     * Gets the designated column`s suggested title for use in printouts and
     * displays. The suggested title is usually specified by the SQL <code>AS</code>
     * clause.  If a SQL <code>AS</code> is not specified, the value returned from
     * <code>getColumnLabel</code> will be the same as the value returned by the
     * <code>getColumnName</code> method.
     *
     * @param column the first column is 1, the second is 2, ...
     * @return the suggested column title
     * @exception SQLException if a database access error occurs
     */
    String getColumnLabel(int column) throws SQLException;複製程式碼

後面的過程就和昨天講的方案一一模一樣了,不再贅述。

方案三-ResultMap

resultMap 元素是 MyBatis 中最重要最強大的元素。它就是讓你遠離 90%的需要從結果 集中取出資料的 JDBC 程式碼的那個東西, 而且在一些情形下允許你做一些 JDBC 不支援的事 情。 事實上, 編寫相似於對複雜語句聯合對映這些等同的程式碼, 也許可以跨過上千行的程式碼。 ResultMap 的設計就是簡單語句不需要明確的結果對映,而很多複雜語句確實需要描述它們 的關係。
ResultMap是Mybatis中可以完成複雜語句對映的東西,但在我們的日常開發中,我們往往是一個XML對應JavaBeans 或 POJOs(Plain Old Java Objects,普通 Java 物件),並沒有特別複雜的應用,下面也是基於日常的使用,看看簡單的ResultMap在原始碼層面是如何展現的。

<resultMap id="cityMap" type="po.CityPO">
        <result column="id" property="id"/>
        <result column="city_id" property="cityId"/>
        <result column="city_name" property="cityName"/>
        <result column="city_en_name" property="cityEnglishName"/>
</resultMap>
<select id="selectCity" resultMap="cityMap">
        select id,
        city_id,
        city_name,
        city_en_name
        from SU_City
        where id = #{id}
</select>複製程式碼

在resultMap的子元素result對應了result和物件欄位之間的對映,並通過id標示,你在Select語句中指定需要使用的resultMap即可。
原始碼層面的話,依舊在DefaultResultSetHandler的handleResultSets中處理返回集合。

List<ResultMap> resultMaps = mappedStatement.getResultMaps();複製程式碼

在這次的ResultMap中,相比之前方案,其屬性更加的豐富起來。將之前寫的Result的資訊儲存在了resultMappings,idResultMappings等中,以備後續使用。

後續的函式走向和方案一二一致,在建立自動對映的時候出現了不同。

privateList<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix)throwsSQLException {複製程式碼

在這個函式中,會獲取沒有對映過的列名。

final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);複製程式碼

之後會根據resultMap檢視是否有未對映的欄位。

loadMappedAndUnmappedColumnNames(resultMap, columnPrefix);複製程式碼
List<String> mappedColumnNames = new ArrayList<String>();
    List<String> unmappedColumnNames = new ArrayList<String>();
    final String upperColumnPrefix = columnPrefix == null ? null : columnPrefix.toUpperCase(Locale.ENGLISH);
    // 這裡沒有配置字首,根據之前的圖,定義了ResultMap後,會記錄這些已經配置對映的欄位。
    final Set<String> mappedColumns = prependPrefixes(resultMap.getMappedColumns(), upperColumnPrefix);
    for (String columnName : columnNames) {
      // 遍歷列名,如果在已對映的配置中,那麼就加入已經對映的列名資料,
      final String upperColumnName = columnName.toUpperCase(Locale.ENGLISH);
      if (mappedColumns.contains(upperColumnName)) {
        mappedColumnNames.add(upperColumnName);
      } else {
        unmappedColumnNames.add(columnName);
      }
    }
    // 生成未對映和已對映的Map
    mappedColumnNamesMap.put(getMapKey(resultMap, columnPrefix), mappedColumnNames);
    unMappedColumnNamesMap.put(getMapKey(resultMap, columnPrefix), unmappedColumnNames);複製程式碼

如果有沒配置在ResultMap中,且Select出來的,那麼之後也會按照之前方案一那樣,繼續往下走,去物件中尋找對映關係。
由於沒有未對映的欄位,使用自動對映的結果是false。

foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;複製程式碼

之後繼續往下走,使用applyPropertyMappings來建立物件。使用了PropertyMapping。裡面包含了欄位名,列名,欄位的型別和對應的處理器。

遍歷整個Mappings。

Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);複製程式碼

函式裡主要的就是獲取這個欄位對應的型別處理器,防止型別轉換失敗,這一部分下次會專門看一下。

final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
return typeHandler.getResult(rs, column);複製程式碼

TypeHandler就是一個介面,主要完成的工作就是從Result根據列名,獲取相應型別的值,為下一步反射賦值做準備。至於它是怎麼決定為什麼用這個型別的TypeHandler下次再看。最後就是給對應欄位賦值。

metaObject.setValue(property, value);複製程式碼

最後就完成了整個類的賦值。

總結

大致上,Mybatis完成對映主要是兩種方式。

  1. 只根據列名,利用自動對映,根據反射類的資訊,得到列名和欄位之間的關係,使用對應的TypeHandler,完成欄位的賦值。

  2. 使用ResultMap預先定義好對映關係,也是最後根據TypeHandler和反射,完成欄位的賦值。
    我個人感覺就簡單的用法來說,兩者都可以,在一次會話中,Configuration中的ResultMap關係建立好,在每一次查詢的時候就不用再去重新建立了,直接用就行。而自動對映的話,執行過一次後,也會在會話中建立自動對映的快取。所以沒什麼差別。但如果複雜的對映的話,就非ResultMap莫屬啦。具體可以參考Mybatis文件關於對映的章節,因為目前用不到比較複雜的對映, 不做深究了。

    www.mybatis.org/mybatis-3/z…

下次去看一下Mybatis是如何知道資料庫中的資料和欄位應該用什麼樣的型別轉換器的。

相關文章