Mybatis原始碼分析(7)—— 結果集處理
解析封裝
ResultMap 是和結果集相關的東西,最初在解析 XML 的時候,於 parseStatementNode 方法中,針對每一個 select 節點進行解析,轉換為 MappedStatement(類似 Spring 的 bean 配置和 BeanDefinition 的關係)。
在 MapperBuilderAssistant 的 addMappedStatement 方法中,構建完statementBuilder,會呼叫 setStatementResultMap 方法給其設定 ResultMap。
setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
private void setStatementResultMap(
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
MappedStatement.Builder statementBuilder) {
resultMap = applyCurrentNamespace(resultMap, true);
List<ResultMap> resultMaps = new ArrayList<ResultMap>();
if (resultMap != null) {
String[] resultMapNames = resultMap.split(",");
for (String resultMapName : resultMapNames) {
try {
resultMaps.add(configuration.getResultMap(resultMapName.trim()));
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("Could not find result map " + resultMapName, e);
}
}
} else if (resultType != null) {
ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(
configuration,
statementBuilder.id() + "-Inline",
resultType,
new ArrayList<ResultMapping>(),
null);
resultMaps.add(inlineResultMapBuilder.build());
}
statementBuilder.resultMaps(resultMaps);
statementBuilder.resultSetType(resultSetType);
}
首先檢查 resultMap,根據名稱去 configuration 中取出對應的 resultMap 放到集合中;
如果 resultMap 不存在,就檢查 resultType ,然後利用這個 resultType 構造一個 resultMap,如果兩個都沒有,那就走著瞧(resultMaps集合為空)。
中期呼叫
在 PreparedStatementHandler 的 query 方法中:
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
而 resultSetHandler 於基類 BaseStatementHandler 中構造:
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
根據是否有巢狀的 ResultMaps 來確定建立 NestedResultSetHandler 還是 FastResultSetHandler:
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = mappedStatement.hasNestedResultMaps() ? new NestedResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql,
rowBounds) : new FastResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
影響這個判斷的有幾個點:
- 解析時期
public Builder resultMaps(List<ResultMap> resultMaps) {
mappedStatement.resultMaps = resultMaps;
for (ResultMap resultMap : resultMaps) {
mappedStatement.hasNestedResultMaps = mappedStatement.hasNestedResultMaps || resultMap.hasNestedResultMaps();
}
return this;
}
- getBoundSql時期
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
後期處理
NestedResultSetHandler繼承自FastResultSetHandler ,二者在handleResultSets是一致的(只是重寫了 handleRowValues 等方法):
public List<Object> handleResultSets(Statement stmt) throws SQLException {
final List<Object> multipleResults = new ArrayList<Object>();
final List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
int resultSetCount = 0;
ResultSet rs = stmt.getResultSet();
while (rs == null) {
// move forward to get the first resultset in case the driver
// doesn't return the resultset as the first result (HSQLDB 2.1)
if (stmt.getMoreResults()) {
rs = stmt.getResultSet();
} else {
if (stmt.getUpdateCount() == -1) {
// no more results. Must be no resultset
break;
}
}
}
// 驗證是否定義了resultType或者resultMap
validateResultMapsCount(rs, resultMapCount);
while (rs != null && resultMapCount > resultSetCount) {
final ResultMap resultMap = resultMaps.get(resultSetCount);
ResultColumnCache resultColumnCache = new ResultColumnCache(rs.getMetaData(), configuration);
handleResultSet(rs, resultMap, multipleResults, resultColumnCache);
rs = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
return collapseSingleResultList(multipleResults);
}
處理模板
- validateResultMapsCount
resultMapCount 就是之前解析時構造的那個 ResultMap 集合的 size,如果沒有配置 ResultMap 或 ResultType,這裡就要報錯了:
A query was run and no Result Maps were found for the Mapped Statement ……
- handleResultSet
傳入構建的List型別的集合 multipleResults,呼叫 handleRowValues 處理從 ResultSet 獲取的結果集。
- collapseSingleResultList
這個就不說了。
細節分析
FastResultSetHandler 用得相對較多,這裡只針對其 handleRowValues 處理結果行進行分析:
protected void handleRowValues(ResultSet rs, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultColumnCache resultColumnCache) throws SQLException {
final DefaultResultContext resultContext = new DefaultResultContext();
skipRows(rs, rowBounds);
while (shouldProcessMoreRows(rs, resultContext, rowBounds)) {
final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rs, resultMap, null);
Object rowValue = getRowValue(rs, discriminatedResultMap, null, resultColumnCache);
callResultHandler(resultHandler, resultContext, rowValue);
}
}
1、RowBounds 與 分頁
skipRows 就是跳過行,基於查詢結果的分頁使用,其中會直接呼叫 ResultSet 的 absolute 方法:
rs.absolute(rowBounds.getOffset());
而 shouldProcessMoreRows 方法就是判斷是否應該獲取更多的行:
protected boolean shouldProcessMoreRows(ResultSet rs, ResultContext context, RowBounds rowBounds) throws SQLException {
return !context.isStopped() && rs.next() && context.getResultCount() < rowBounds.getLimit();
}
也對應了分頁的limit欄位,這種分頁可以說是偽分頁,查出來再分頁。所以我們一般使用外掛的形式來實現分頁,基於sql的動態替換。
2、獲取行資料
如果應該獲取更多的行,就會逐行處理結果:
protected Object getRowValue(ResultSet rs, ResultMap resultMap, CacheKey rowKey, ResultColumnCache resultColumnCache) throws SQLException {
final ResultLoaderMap lazyLoader = instantiateResultLoaderMap();
Object resultObject = createResultObject(rs, resultMap, lazyLoader, null, resultColumnCache);
if (resultObject != null && !typeHandlerRegistry.hasTypeHandler(resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(resultObject);
boolean foundValues = resultMap.getConstructorResultMappings().size() > 0;
if (shouldApplyAutomaticMappings(resultMap, !AutoMappingBehavior.NONE.equals(configuration.getAutoMappingBehavior()))) {
final List<String> unmappedColumnNames = resultColumnCache.getUnmappedColumnNames(resultMap, null);
foundValues = applyAutomaticMappings(rs, unmappedColumnNames, metaObject, null, resultColumnCache) || foundValues;
}
final List<String> mappedColumnNames = resultColumnCache.getMappedColumnNames(resultMap, null);
foundValues = applyPropertyMappings(rs, resultMap, mappedColumnNames, metaObject, lazyLoader, null) || foundValues;
foundValues = (lazyLoader != null && lazyLoader.size() > 0) || foundValues;
resultObject = foundValues ? resultObject : null;
return resultObject;
}
return resultObject;
}
主要就是根據相關引數建立結果物件,這裡又分多鐘情況:
- 原始物件的處理(如 Integer 等基本型別物件)
利用 ResultMap 中儲存的返回結果型別資訊,直接返回相應的值。
- 非原始物件(是自定義的物件)處理
運用反射構造實體物件(相當於一個帶有零值的初始物件)。然後用該物件構造一個 MetaObject ,並檢視是否需要根據對映自動填充物件。
如果需要通過自動對映來填充,取出 List 型別的屬性名,根據這個同 SQL 中查出來一致的欄位名,去 ResultSet 中拿出來然後填充到物件中,相關程式碼如下:
final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
if (property != null) {
final Class<?> propertyType = metaObject.getSetterType(property);
if (typeHandlerRegistry.hasTypeHandler(propertyType)) {
final TypeHandler<?> typeHandler = resultColumnCache.getTypeHandler(propertyType, columnName);
final Object value = typeHandler.getResult(rs, columnName);
if (value != null || configuration.isCallSettersOnNulls()) { // issue #377, call setter on nulls
metaObject.setValue(property, value);
foundValues = true;
}
}
}
這個處理完後,才是非對映列的處理。
3、結果對映
resultMap 元素是 MyBatis 中最重要最強大的元素。它可以讓你從 90% 的 JDBC ResultSets 資料提取程式碼中解放出來, 並在一些情形下允許你做一些 JDBC 不支援的事情。
在 handleResultSets 方法中,取 ResultSet 元資訊構造了一個 ResultColumnCache:
ResultColumnCache resultColumnCache = new ResultColumnCache(rs.getMetaData(), configuration);
後面在對映列的時候分別呼叫了 getUnmappedColumnNames 和 getMappedColumnNames 方法來取列集合,對應於 ResultColumnCache 的 unMappedColumnNamesMap 和 mappedColumnNamesMap。如果集合為空,將呼叫下面的方法來構造:
private void loadMappedAndUnmappedColumnNames(ResultMap resultMap, String columnPrefix) throws SQLException {
List<String> mappedColumnNames = new ArrayList<String>();
List<String> unmappedColumnNames = new ArrayList<String>();
final String upperColumnPrefix = columnPrefix == null ? null : columnPrefix.toUpperCase(Locale.ENGLISH);
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);
}
}
mappedColumnNamesMap.put(getMapKey(resultMap, columnPrefix), mappedColumnNames);
unMappedColumnNamesMap.put(getMapKey(resultMap, columnPrefix), unmappedColumnNames);
}
通常情況下,loadMappedAndUnmappedColumnNames 只會呼叫一次(多行結果的列名是一樣的),所以 ResultColumnCache 名副其實。
它區分是否是滿足對映的列名是根據 mappedColumns 集合中是否有來判斷的,而 mappedColumns 對應於 ResultMap 配置中的 column 屬性。
所以自動對映針對的是沒有配置 ResultMap 而使用了 ResultType 的情況;或者配置並使用了 ResultMap,但是 ResultMap 中的 column 屬性集合不包含實際SQL欄位名,也就是對不上的情況。這個時候是需要直接操作這個欄位名,可能需要去除下劃線等基於規則的演變,最後通過 MetaObject 來賦值。
那麼非自動對映呢?就是配置並使用了 ResultMap,這個簡單多了,直接按你配置的來,取 ResultMap 屬性配置下的 property 名稱,這個就是你實際的物件屬性名,然後也是通過 MetaObject 來賦值。
分析到這裡,已經比較明朗了,整體的脈絡也在腦海中勾勒出來了。ResultMap 實際遠不止這些,至於高階功能到時候用起來再具體分析吧。