Mybatis查詢結(jié)果為空時,為什么返回值為NULL或空集合?

文章來源:https://c1n.cn/6l7NH
背景
JDBC 中的 ResultSet 簡介
簡單映射
回歸最初的問題:查詢結(jié)果為空時的返回值
結(jié)論
背景
一行數(shù)據(jù)記錄如何映射成一個 Java 對象,這種映射機(jī)制是 MyBatis 作為 ORM 框架的核心功能之一,也是我們這篇文章需要學(xué)習(xí)的內(nèi)容。
開始前我們先看一個問題:

你是否曾經(jīng)在學(xué)習(xí) Mybatis 的時候跟我有一樣的疑問,什么情況下返回 null,什么時候是空集合,為什么會是這種結(jié)果?那么你覺得上述這種回答能說服你嘛?
我想應(yīng)該不能吧,除非親眼所見,否則真的很難確認(rèn)別人說的是對還是錯(畢竟網(wǎng)上的答案真的千奇百怪,啥都有,已經(jīng)不是第一次發(fā)現(xiàn)一些錯誤的說法被廣泛流傳了),那么這篇文章我們就簡單的分析一下。
看完這篇你就知道查詢結(jié)果為空時候?yàn)槭裁醇蠒强占隙皇?NULL,而對象為什么會是 NULL 了。
PS:對過程不感興趣的可以直接跳到最后看結(jié)論。
JDBC 中的 ResultSet 簡介
你如果有 JDBC 編程經(jīng)驗(yàn)的話,應(yīng)該知道在數(shù)據(jù)庫中執(zhí)行一條 Select 語句通常只能拿到一個 ResultSet,而結(jié)果集 ResultSet 是數(shù)據(jù)中查詢結(jié)果返回的一種對象,可以說結(jié)果集是一個存儲查詢結(jié)果的對象。
while(rs.next()){
// 獲取數(shù)據(jù)
int id = rs.getInt(1);
String name = rs.getString("name");
System.out.println(id + "---" + name);
}
結(jié)果集處理入口 ResultSetHandler
當(dāng) MyBatis 執(zhí)行完一條 select 語句,拿到 ResultSet 結(jié)果集之后,會將其交給關(guān)聯(lián)的 ResultSetHandler 進(jìn)行后續(xù)的映射處理。
在 MyBatis 中只提供了一個 ResultSetHandler 接口實(shí)現(xiàn),即 DefaultResultSetHandler。
下面我們就以 DefaultResultSetHandler 為中心,介紹 MyBatis 中 ResultSet 映射的核心流程。
public interface ResultSetHandler {
// 將ResultSet映射成Java對象
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
// 將ResultSet映射成游標(biāo)對象
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
// 處理存儲過程的輸出參數(shù)
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
| handleResultSets
DefaultResultSetHandler 實(shí)現(xiàn)的 handleResultSets() 方法就支持多個 ResultSet 的處理,里面所調(diào)用的 handleResultSet() 方法就是負(fù)責(zé)處理單個 ResultSet。
public List<Object> handleResultSets(Statement stmt) throws SQLException {
// 用于記錄每個ResultSet映射出來的Java對象
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
// 從Statement中獲取第一個ResultSet,其中對不同的數(shù)據(jù)庫有兼容處理邏輯,
// 這里拿到的ResultSet會被封裝成ResultSetWrapper對象返回
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 獲取這條SQL語句關(guān)聯(lián)的全部ResultMap規(guī)則。如果一條SQL語句能夠產(chǎn)生多個ResultSet,
// 那么在編寫Mapper.xml映射文件的時候,我們可以在SQL標(biāo)簽的resultMap屬性中配置多個
// <resultMap>標(biāo)簽的id,它們之間通過","分隔,實(shí)現(xiàn)對多個結(jié)果集的映射
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) { // 遍歷ResultMap集合
ResultMap resultMap = resultMaps.get(resultSetCount);
// 根據(jù)ResultMap中定義的映射規(guī)則處理ResultSet,并將映射得到的Java對象添加到
// multipleResults集合中保存
handleResultSet(rsw, resultMap, multipleResults, null);
// 獲取下一個ResultSet
rsw = getNextResultSet(stmt);
// 清理nestedResultObjects集合,這個集合是用來存儲中間數(shù)據(jù)的
cleanUpAfterHandlingResultSet();
resultSetCount++; // 遞增ResultSet編號
}
// 下面這段邏輯是根據(jù)ResultSet的名稱處理嵌套映射,你可以暫時不關(guān)注這段代碼,
// 嵌套映射會在后面詳細(xì)介紹
...
// 返回全部映射得到的Java對象
return collapseSingleResultList(multipleResults);
}
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
if (resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
// 將該ResultSet結(jié)果集處理完后的List對象放入multipleResults中,這樣就可以支持返回多個結(jié)果集了
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
這里獲取到的 ResultSet 對象,會被包裝成 ResultSetWrapper 對象,而 ResultSetWrapper 主要用于封裝 ResultSet 的一些元數(shù)據(jù),其中記錄了 ResultSet 中每列的名稱、對應(yīng)的 Java 類型、JdbcType 類型以及每列對應(yīng)的 TypeHandler。
| DefaultResultHandler 和 DefaultResultContext
在開始詳細(xì)介紹映射流程中的每一步之前,我們先來看一下貫穿整個映射過程的兩個輔助對象 DefaultResultHandler 和 DefaultResultContext。
在 DefaultResultSetHandler 中維護(hù)了一個 resultHandler 字段(ResultHandler 接口類型),它默認(rèn)情況下為空。
比如 DefaultSqlSession#selectList() 中傳遞的值就是 ResultHandler NO_RESULT_HANDLER = null;
它有兩個實(shí)現(xiàn)類:
DefaultResultHandler 實(shí)現(xiàn)的底層使用 ArrayList<Object> 存儲單個結(jié)果集映射得到的 Java 對象列表。
DefaultMapResultHandler 實(shí)現(xiàn)的底層使用 Map<K, V> 存儲映射得到的 Java 對象,其中 Key 是從結(jié)果對象中獲取的指定屬性的值,Value 就是映射得到的 Java 對象。
DefaultResultContext 對象,它的生命周期與一個 ResultSet 相同,每從 ResultSet 映射得到一個 Java 對象都會暫存到 DefaultResultContext 中的 resultObject 字段,等待后續(xù)使用。
同時 DefaultResultContext 還可以計算從一個 ResultSet 映射出來的對象個數(shù)(依靠 resultCount 字段統(tǒng)計)。
| 多結(jié)果集返回
數(shù)據(jù)庫支持同時返回多個 ResultSet 的場景,例如在存儲過程中執(zhí)行多條 Select 語句。
MyBatis 作為一個通用的持久化框架,不僅要支持常用的基礎(chǔ)功能,還要對其他使用場景進(jìn)行全面的支持。
private List<Object> collapseSingleResultList(List<Object> multipleResults) {
// 如果只有一個結(jié)果集就返回一個,否則直接通過List列表返回多個結(jié)果集
return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
}
multipleResults 里有多少個 List 列表取決于 handleResultSet() 方法里的 resultHandler == null 的判斷。
默認(rèn)情況下沒有設(shè)置 resultHandler 的話,那每處理一個 ResultSet 就會添加結(jié)果到 multipleResults 中, 此時 multipleResults.size() == 1 必然是不等于 1 的。
注:感興趣的可以自行查看 resultHandler 什么時候會不為空。
簡單映射
DefaultResultSetHandler 是如何處理單個結(jié)果集的,這部分邏輯的入口是 handleResultSet() 方法,其中會根據(jù)第四個參數(shù),也就是 parentMapping,判斷當(dāng)前要處理的 ResultSet 是嵌套映射,還是外層映射。
無論是處理外層映射還是嵌套映射,都會依賴 handleRowValues() 方法完成結(jié)果集的處理。
通過方法名也可以看出,handleRowValues() 方法是處理多行記錄的,也就是一個結(jié)果集。
handleRowValuesForNestedResultMap() 方法處理包含嵌套映射的 ResultMap,是否為嵌套查詢結(jié)果集,看 <resultMap> 聲明時,是否包含 association、collection、case 關(guān)鍵字。
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);
}
}
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
// 跳過多余的記錄
skipRows(resultSet, rowBounds);
// 檢測是否還有需要映射的數(shù)據(jù)
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
// 處理映射中用到的 Discriminator,決定此次映射實(shí)際使用的 ResultMap。
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
該方法的核心步驟可總結(jié)為如下:
執(zhí)行 skipRows() 方法跳過多余的記錄,定位到指定的行。
通過 shouldProcessMoreRows() 方法,檢測是否還有需要映射的數(shù)據(jù)記錄。
如果存在需要映射的記錄,則先通過 resolveDiscriminatedResultMap() 方法處理映射中用到的 Discriminator,決定此次映射實(shí)際使用的 ResultMap。
通過 getRowValue() 方法對 ResultSet 中的一行記錄進(jìn)行映射,映射規(guī)則使用的就是步驟 3 中確定的 ResultMap。
執(zhí)行 storeObject() 方法記錄步驟 4 中返回的、映射好的 Java 對象。
| ResultSet 的預(yù)處理
我們可以通過 RowBounds 指定 offset、limit 參數(shù)實(shí)現(xiàn)分頁的效果。
這里的 skipRows() 方法就會根據(jù) RowBounds 移動 ResultSet 的指針到指定的數(shù)據(jù)行,這樣后續(xù)的映射操作就可以從這一行開始。
通過上述分析我們可以看出,通過 RowBounds 實(shí)現(xiàn)的分頁功能實(shí)際上還是會將全部數(shù)據(jù)加載到 ResultSet 中,而不是只加載指定范圍的數(shù)據(jù)所以我們可以認(rèn)為 RowBounds 實(shí)現(xiàn)的是一種“假分頁”。
這種“假分頁”在數(shù)據(jù)量大的時候,性能就會很差,在處理大數(shù)據(jù)量分頁時,建議通過 SQL 語句 where 條件 + limit 的方式實(shí)現(xiàn)分頁。
| 確定 ResultMap
public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException {
// 用于維護(hù)處理過的ResultMap唯一標(biāo)識
Set<String> pastDiscriminators = new HashSet<>();
// 獲取ResultMap中的Discriminator對象,這是通過<resultMap>標(biāo)簽中的<discriminator>標(biāo)簽解析得到的
Discriminator discriminator = resultMap.getDiscriminator();
while (discriminator != null) {
// 獲取當(dāng)前待映射的記錄中Discriminator要檢測的列的值
final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
// 根據(jù)上述列值確定要使用的ResultMap的唯一標(biāo)識
final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
if (configuration.hasResultMap(discriminatedMapId)) {
// 從全局配置對象Configuration中獲取ResultMap對象
resultMap = configuration.getResultMap(discriminatedMapId);
// 記錄當(dāng)前Discriminator對象
Discriminator lastDiscriminator = discriminator;
// 獲取ResultMap對象中的Discriminator
discriminator = resultMap.getDiscriminator();
// 檢測Discriminator是否出現(xiàn)了環(huán)形引用
if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
break;
}
} else {
break;
}
}
// 返回最終要使用的ResultMap
return resultMap;
}
至于 ResultMap 對象是怎么創(chuàng)建的,感興趣的可以自行從 XMLMapperBuilder#resultMapElements() 方法去了解一下,這里不再贅述。
| 創(chuàng)建映射結(jié)果對象
確定了當(dāng)前記錄使用哪個 ResultMap 進(jìn)行映射之后,要做的就是按照 ResultMap 規(guī)則進(jìn)行各個列的映射,得到最終的 Java 對象,這部分邏輯是在 getRowValue() 方法完成的。
其核心步驟如下:
首先根據(jù) ResultMap 的 type 屬性值創(chuàng)建映射的結(jié)果對象。
然后根據(jù) ResultMap 的配置以及全局信息,決定是否自動映射 ResultMap 中未明確映射的列。
接著根據(jù) ResultMap 映射規(guī)則,將 ResultSet 中的列值與結(jié)果對象中的屬性值進(jìn)行映射。
最后返回映射的結(jié)果對象,如果沒有映射任何屬性,則需要根據(jù)全局配置決定如何返回這個結(jié)果值,這里不同場景和配置,可能返回完整的結(jié)果對象、空結(jié)果對象或是 null。
這個可以關(guān)注 mybatis 配置中的 returnInstanceForEmptyRow 屬性,它默認(rèn)為 false。
當(dāng)返回行的所有列都是空時,MyBatis 默認(rèn)返回 null。當(dāng)開啟這個設(shè)置時,MyBatis會返回一個空實(shí)例。
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 根據(jù)ResultMap的type屬性值創(chuàng)建映射的結(jié)果對象
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
// 根據(jù)ResultMap的配置以及全局信息,決定是否自動映射ResultMap中未明確映射的列
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
// 根據(jù)ResultMap映射規(guī)則,將ResultSet中的列值與結(jié)果對象中的屬性值進(jìn)行映射
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
// 如果沒有映射任何屬性,需要根據(jù)全局配置決定如何返回這個結(jié)果值,
// 這里不同場景和配置,可能返回完整的結(jié)果對象、空結(jié)果對象或是null
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
| 自動映射
創(chuàng)建完結(jié)果對象之后,下面就可以開始映射各個字段了。在簡單映射流程中,會先通過 shouldApplyAutomaticMappings() 方法檢測是否開啟了自動映射。
主要檢測以下兩個地方:
檢測當(dāng)前使用的 ResultMap 是否配置了 autoMapping 屬性,如果是,則直接根據(jù)該 autoMapping 屬性的值決定是否開啟自動映射功能。
檢測 mybatis-config.xml 的 <settings> 標(biāo)簽中配置的 autoMappingBehavior 值,決定是否開啟自動映射功能。NONE 表示關(guān)閉自動映射;PARTIAL 只會自動映射沒有定義嵌套結(jié)果映射的字段;FULL 會自動映射任何復(fù)雜的結(jié)果集(無論是否嵌套)。
| 正常映射
完成自動映射之后,MyBatis 會執(zhí)行 applyPropertyMappings() 方法處理 ResultMap 中明確要映射的列。
| 存儲對象
通過上述 5 個步驟,我們已經(jīng)完成簡單映射的處理,得到了一個完整的結(jié)果對象。
private void storeObject(...) throws SQLException {
if (parentMapping != null) {
// 嵌套查詢或嵌套映射的場景,此時需要將結(jié)果對象保存到外層對象對應(yīng)的屬性中
linkToParents(rs, parentMapping, rowValue);
} else {
// 普通映射(沒有嵌套映射)或是嵌套映射中的外層映射的場景,此時需要將結(jié)果對象保存到ResultHandler中
callResultHandler(resultHandler, resultContext, rowValue);
}
}
這里處理的簡單映射,如果是一個嵌套映射中的子映射,那么我們就需要將結(jié)果對象保存到外層對象的屬性中。
如果是一個普通映射或是外層映射的結(jié)果對象,那么我們就需要將結(jié)果對象保存到 ResultHandler 中。
回歸最初的問題:查詢結(jié)果為空時的返回值
| 返回結(jié)果為單行數(shù)據(jù)
可以從 ResultSetHandler的handleResultSets 方法開始分析。
multipleResults 用于記錄每個 ResultSet 映射出來的 Java 對象,注意這里是每個 ResultSet,也就說可以有多個結(jié)果集。
我們可以看到 DefaultSqlSession#selectOne() 方法,我們先說結(jié)論:因?yàn)橹挥幸粋€ ResultSet 結(jié)果集,那么返回值為 null。
步驟如下:
// 檢測是否還有需要映射的數(shù)據(jù)
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next())
public class DefaultResultHandler implements ResultHandler<Object> {
// 默認(rèn)是空集合
private final List<Object> list;
public DefaultResultHandler() {
list = new ArrayList<>();
}
@SuppressWarnings("unchecked")
public DefaultResultHandler(ObjectFactory objectFactory) {
list = objectFactory.create(List.class);
}
@Override
public void handleResult(ResultContext<? extends Object> context) {
list.add(context.getResultObject());
}
public List<Object> getResultList() {
return list;
}
}
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
| 返回結(jié)果為多行數(shù)據(jù)
那么我們看到 DefaultSqlSession#selectList() 方法,先說結(jié)論:返回值為空集合而不是 NULL。
前面都同理,感興趣的可以自己順著 executor.query 一路往下看,會發(fā)現(xiàn)最后就是調(diào)用的 resultSetHandler.handleResultSets() 方法。
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
結(jié)論
看到這,我們在反過來看上面截圖里的答案,什么返回值是 Java 集合會先初始化??而且如果是 Map 作為返回值的話,那直接是返回的 NULL 好吧,簡直是錯的離譜!
如果返回值是 Java 集合類型,如 List、Map,會先初始化(new 一個集合對象),再把結(jié)果添加進(jìn)去;如果返回值是普通對象,查詢不到時,返回值是 null。
其實(shí)不管你是查單行記錄還是多行記錄,對于 Mybatis 來說都會放到 DefaultResultHandler 中去,而 DefaultResultHandler 又是用 List 存儲結(jié)果。
所以不管是集合類型還是普通對象,Mybatis 都會先初始化一個 List 存儲結(jié)果,然后返回值為普通對象且查為空的時候,selectOne 會判斷然后直接返回 NULL 值。
而返回值為集合對象且查為空時,selectList 會把這個存儲結(jié)果的 List 對象直接返回,此時這個 List 就是個空集合。
