前言
考慮到在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完成對映主要是兩種方式。
只根據列名,利用自動對映,根據反射類的資訊,得到列名和欄位之間的關係,使用對應的TypeHandler,完成欄位的賦值。
使用ResultMap預先定義好對映關係,也是最後根據TypeHandler和反射,完成欄位的賦值。
我個人感覺就簡單的用法來說,兩者都可以,在一次會話中,Configuration中的ResultMap關係建立好,在每一次查詢的時候就不用再去重新建立了,直接用就行。而自動對映的話,執行過一次後,也會在會話中建立自動對映的快取。所以沒什麼差別。但如果複雜的對映的話,就非ResultMap莫屬啦。具體可以參考Mybatis文件關於對映的章節,因為目前用不到比較複雜的對映, 不做深究了。
下次去看一下Mybatis是如何知道資料庫中的資料和欄位應該用什麼樣的型別轉換器的。