MyBatis在轉(zhuǎn)換查詢結(jié)果到需要的Java業(yè)務(wù)對(duì)象時(shí)做了三件事:
- 解決了數(shù)據(jù)庫(kù)列名到Java列名的映射。
- 解決了數(shù)據(jù)庫(kù)類型到Java類型的轉(zhuǎn)換工作莉钙。
- 在轉(zhuǎn)換過(guò)程中具備一定的容錯(cuò)能力陕壹。
其實(shí)核心就是:
- 數(shù)據(jù)庫(kù)中的列名怎么和對(duì)象中的字段對(duì)應(yīng)起來(lái)。
- 數(shù)據(jù)庫(kù)中的列的類型怎么轉(zhuǎn)換到合適的Java類型浸剩,不引起轉(zhuǎn)換失敗可帽。
我們先來(lái)看第一點(diǎn)娄涩,數(shù)據(jù)庫(kù)中的列名怎么和對(duì)象中的字段對(duì)應(yīng)起來(lái)。首先是PO(Persistant Object) CityPO里面有五個(gè)字段:
public class CityPO {
Integer id;
Long cityId;
String cityName;
String cityEnName;
String cityPyName;
}
本次要查詢的數(shù)據(jù)庫(kù)中的列名如下所示:
mysql> mysql> desc SU_City;
+--------------+-------------+------+-----+-------------------+-----------------------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------+------+-----+-------------------+-----------------------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| city_id | int(11) | NO | UNI | NULL | |
| city_name | varchar(20) | NO | | | |
| city_en_name | varchar(20) | NO | | | |
| city_py_name | varchar(50) | NO | | | |
| create_time | datetime | NO | | CURRENT_TIMESTAMP | |
| updatetime | datetime | NO | MUL | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+--------------+-------------+------+-----+-------------------+-----------------------------+
7 rows in set (0.01 sec)
我們是按照駝峰式命名映跟,把數(shù)據(jù)庫(kù)中的列名對(duì)應(yīng)到了對(duì)象的字段名蓄拣。如下是MyBatis的接口類和映射文件。
接口類:
public interface CityMapper {
CityPO selectCity(int id);
}
映射文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mapper.CityMapper">
<select id="selectCity" resultType="po.CityPO">
select id,city_id,city_name,city_en_name from SU_City where id = #{id}
</select>
</mapper>
在上面的映射文件中努隙,namespace指定了這個(gè)接口類的全限定類名球恤,緊隨其后的select代表是select語(yǔ)句,id是接口類中函數(shù)的名字荸镊,resultType代表了從這條語(yǔ)句中返回的期望類型的類的完全限定名或別名咽斧,在此例子中是我們的業(yè)務(wù)對(duì)象CityPO的類路徑。
主要有三種方案:
- 駝峰式命名開(kāi)關(guān)躬存,或者不開(kāi)——數(shù)據(jù)庫(kù)列和字段名全一致收厨。
- Select時(shí)指定AS。
- resultMap 最穩(wěn)健优构。
1. 駝峰命名開(kāi)關(guān)
因?yàn)镃ityPO的列名是完全根據(jù)數(shù)據(jù)庫(kù)列名駝峰式命名后得到的诵叁,因此MyBatis提供了一個(gè)配置項(xiàng)。開(kāi)啟開(kāi)配置項(xiàng)后钦椭,在匹配時(shí)拧额,能夠根據(jù)數(shù)據(jù)庫(kù)列名找到對(duì)應(yīng)對(duì)應(yīng)的駝峰式命名后的字段碑诉。
<settings>
<!-- 開(kāi)啟駝峰,開(kāi)啟后侥锦,只要數(shù)據(jù)庫(kù)字段和對(duì)象屬性名字母相同进栽,無(wú)論中間加多少下劃線都可以識(shí)別 -->
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
從源碼角度解讀一下,MyBatis處理ResultSet的映射默認(rèn)都在DefaultResultSetHandler中完成恭垦。
處理行數(shù)據(jù)的源碼主要在下面的函數(shù)里進(jìn)行快毛,由于我們?cè)谟成湮募袥](méi)有定義額外的ResultMap,因此會(huì)直接進(jìn)入else分支的代碼番挺。
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);
}
}
進(jìn)入handleRowValuesForSimpleResultMap中唠帝,主要處理函數(shù)如下,在這里完成了對(duì)象的生成及賦值玄柏。
Object rowValue = getRowValue(rsw, discriminatedResultMap);
在這里先創(chuàng)建了對(duì)象的實(shí)例襟衰,然后獲取了對(duì)象的元信息,為反射賦值做準(zhǔn)備粪摘。
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
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, null) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = (foundValues || configuration.isReturnInstanceForEmptyRow()) ? rowValue : null;
}
return rowValue;
}
在applyAutomaticMappings完成了整個(gè)過(guò)程瀑晒,我們進(jìn)去探一探。
就是下面這個(gè)函數(shù)創(chuàng)建好了映射關(guān)系徘意,這個(gè)函數(shù)的下半部分是完成賦值的苔悦,映射的部分下次會(huì)詳細(xì)分析。
List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
在這個(gè)方法里椎咧,上半部分是生成了數(shù)據(jù)庫(kù)的列名玖详,在這個(gè)函數(shù)中找到了對(duì)應(yīng)的字段名。
final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
我們進(jìn)去看一看邑退,它傳進(jìn)了生成好的數(shù)據(jù)庫(kù)列名,傳進(jìn)了前面提到的是否根據(jù)駝峰式命名映射開(kāi)關(guān)的值劳澄。
事實(shí)證明地技,真的很簡(jiǎn)單,往下看秒拔,就是把下劃線都去了莫矗。
public String findProperty(String name, boolean useCamelCaseMapping) {
if (useCamelCaseMapping) {
name = name.replace("_", "");
}
return findProperty(name);
}
隱隱覺(jué)得是不是大小寫(xiě)不敏感啊,繼續(xù)往下看砂缩,這里返回找到的字段名作谚。
private StringBuilder buildProperty(String name, StringBuilder builder) {
......
String propertyName = reflector.findPropertyName(name);
if (propertyName != null) {
builder.append(propertyName);
}
return builder;
}
好了,真相大白庵芭,就是大小寫(xiě)不敏感的妹懒。
public String findPropertyName(String name) {
return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH));
}
2. Select .... AS
當(dāng)我們的數(shù)據(jù)庫(kù)列名和對(duì)象字段之間不是駝峰式命名的關(guān)系,可以在Select時(shí)使用AS双吆,使得列名和對(duì)象名匹配上眨唬。
映射文件中是本次會(huì)執(zhí)行的sql会前,我們會(huì)查出id,city_id匾竿,city_name瓦宜,city_en_name。 按照開(kāi)啟的駝峰式命名開(kāi)關(guān)岭妖,我們會(huì)對(duì)應(yīng)到對(duì)象的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>
不過(guò)在這次,我們對(duì)PO做了小小的改動(dòng)废离,把cityEnName改成了cityEnglishName侄泽。
public class CityPO {
Integer id;
Long cityId;
String cityName;
String cityEnglishName; // 由cityEnName改成了cityEnglishName
}
由于找不到匹配的列,cityEnlishName肯定沒(méi)法被反射賦值蜻韭,因此值為Null悼尾。
CityPO{id=2, cityId=2, cityName='北京', cityEnglishName='null'}
解決辦法:在Select字段的時(shí)候使用AS,下面是改動(dòng)后的映射文件肖方。
<select id="selectCity" resultType="po.CityPO">
select id,
city_id,
city_name,
city_en_name AS cityEnglishName
from SU_City
where id = #{id}
</select>
改動(dòng)后執(zhí)行得到的結(jié)果如下闺魏。
CityPO{id=2, cityId=2, cityName='北京', cityEnglishName='beijing'}
那么我們來(lái)看看它是如何生效的,主要的代碼在哪里俯画。上面我們第一個(gè)介紹的函數(shù)handleRowValues中傳入了參數(shù)rsw析桥,它是對(duì)ResultSet的一個(gè)包裝,在這個(gè)包裝里艰垂,完成了具體使用哪個(gè)名字作為數(shù)據(jù)庫(kù)的列名泡仗。
final ResultSetWrapper rsw = new ResultSetWrapper(rs, configuration);
handleRowValues(rsw, resultMap, resultHandler, new RowBounds(), null);
在這個(gè)構(gòu)造函數(shù)當(dāng)中,我們會(huì)獲取數(shù)據(jù)庫(kù)的列名猜憎,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));
}
在添加列名時(shí)胰柑,會(huì)從配置中獲取是否使用類標(biāo)簽截亦,isUseColumnLabel,默認(rèn)為true柬讨。根據(jù)Javadoc崩瓤,這個(gè)ColumnLabel就是AS后的那個(gè)名字,如果沒(méi)有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;
后面的過(guò)程就和上面方案一一模一樣了,不再贅述蔗牡。
3. ResultMap
resultMap 元素是 MyBatis 中最重要最強(qiáng)大的元素肾扰。它可以讓你從 90% 的 JDBC ResultSets 數(shù)據(jù)提取代碼中解放出來(lái)畴嘶,并在一些情形下允許你做一些 JDBC 不支持的事情。 實(shí)際上集晚,在對(duì)復(fù)雜語(yǔ)句進(jìn)行聯(lián)合映射的時(shí)候窗悯,它很可能可以代替數(shù)千行的同等功能的代碼。 ResultMap 的設(shè)計(jì)思想是偷拔,簡(jiǎn)單的語(yǔ)句不需要明確的結(jié)果映射蒋院,而復(fù)雜一點(diǎn)的語(yǔ)句只需要描述它們的關(guān)系就行了。
ResultMap是MyBatis中可以完成復(fù)雜語(yǔ)句映射的東西莲绰,但在我們的日常開(kāi)發(fā)中欺旧,我們往往是一個(gè)XML對(duì)應(yīng)JavaBeans 或 POJOs(Plain Old Java Objects,普通 Java 對(duì)象)蛤签,并沒(méi)有特別復(fù)雜的應(yīng)用辞友,下面也是基于日常的使用,看看簡(jiǎn)單的ResultMap在源碼層面是如何展現(xiàn)的震肮。
<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對(duì)應(yīng)了result和對(duì)象字段之間的映射称龙,并通過(guò)id標(biāo)示,你在Select語(yǔ)句中指定需要使用的resultMap即可戳晌。
源碼層面的話鲫尊,依舊在DefaultResultSetHandler的handleResultSets中處理返回集合。
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
在這次的ResultMap中沦偎,相比之前方案疫向,其屬性更加的豐富起來(lái)。將之前寫(xiě)的Result的信息保存在resultMappings豪嚎,idResultMappings等中搔驼,以備后續(xù)使用。
后續(xù)的函數(shù)走向和方案一二一致侈询,但在創(chuàng)建自動(dòng)映射的時(shí)候出現(xiàn)了不同舌涨。
private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
}
在這個(gè)函數(shù)中,會(huì)獲取沒(méi)有映射過(guò)的列名妄荔。
final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
之后會(huì)根據(jù)resultMap查看是否有未映射的字段泼菌。
loadMappedAndUnmappedColumnNames(resultMap, columnPrefix);
private void loadMappedAndUnmappedColumnNames(ResultMap resultMap, String columnPrefix) throws SQLException {
List<String> mappedColumnNames = new ArrayList<>();
List<String> unmappedColumnNames = new ArrayList<>();
final String upperColumnPrefix = columnPrefix == null ? null : columnPrefix.toUpperCase(Locale.ENGLISH);
// 這里沒(méi)有配置前綴谍肤,根據(jù)之前的圖啦租,定義了ResultMap后,會(huì)記錄這些已經(jīng)配置映射的字段荒揣。
final Set<String> mappedColumns = prependPrefixes(resultMap.getMappedColumns(), upperColumnPrefix);
for (String columnName : columnNames) {
// 遍歷列名篷角,如果在已映射的配置中,那么就加入已經(jīng)映射的列名數(shù)據(jù)
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);
}
如果有沒(méi)配置在ResultMap中系任,且Select出來(lái)的恳蹲,那么之后也會(huì)按照之前方案一那樣虐块,繼續(xù)往下走,從對(duì)象中尋找映射關(guān)系嘉蕾。
由于沒(méi)有未映射的字段贺奠,使用自動(dòng)映射的結(jié)果是false。
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
之后繼續(xù)往下走错忱,使用applyPropertyMappings來(lái)創(chuàng)建對(duì)象儡率。其用到了propertyMappings,里面包含了字段名以清,列名儿普,字段的類型和對(duì)應(yīng)的處理器。
遍歷整個(gè)Mappings掷倔。
Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
函數(shù)里主要的就是獲取這個(gè)字段對(duì)應(yīng)的類型處理器眉孩,防止類型轉(zhuǎn)換失敗,這一部分下次會(huì)專門看一下勒葱。
final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
return typeHandler.getResult(rs, column);
TypeHandler就是一個(gè)接口浪汪,主要完成的工作就是從Result根據(jù)列名,獲取相應(yīng)類型的值错森,為下一步反射賦值做準(zhǔn)備吟宦。至于它是怎么決定為什么用這個(gè)類型的TypeHandler下次再看。
然后就是給對(duì)應(yīng)字段賦值涩维。
metaObject.setValue(property, value);
最后就完成了整個(gè)類的賦值殃姓。
總結(jié)
大致上,MyBatis完成映射主要是兩種方式:
- 只根據(jù)列名瓦阐,利用自動(dòng)映射蜗侈,根據(jù)反射類的信息,得到列名和字段之間的關(guān)系睡蟋,使用對(duì)應(yīng)的TypeHandler完成字段的賦值踏幻。
- 使用ResultMap預(yù)先定義好映射關(guān)系,最后也是根據(jù)TypeHandler和反射完成字段的賦值戳杀。
就簡(jiǎn)單的用法來(lái)說(shuō)该面,兩者都可以。在一次會(huì)話中信卡,Configuration中的ResultMap關(guān)系建立好隔缀,在每一次查詢的時(shí)候就不用再去重新建立了,直接用就行傍菇。而自動(dòng)映射的話猾瘸,執(zhí)行過(guò)一次后,也會(huì)在會(huì)話中建立自動(dòng)映射的緩存,所以沒(méi)什么差別牵触。但如果復(fù)雜的映射的話淮悼,就非ResultMap莫屬啦。具體可以參考MyBatis文檔關(guān)于映射的章節(jié)揽思,因?yàn)槟壳坝貌坏奖容^復(fù)雜的映射袜腥,不做深究了。