詳解MyBatis?ResultSetHandler?結(jié)果集的解析過程
正文
mybatis版本:3.5.12
mybatis通過Executor查詢出結(jié)果后,通常返回的是一個List結(jié)構(gòu),再根據(jù)用戶調(diào)用的API把List結(jié)構(gòu)轉(zhuǎn)為指定結(jié)構(gòu)。
- 比如用戶調(diào)用
SqlSession#selectOne
就是List中只有一條數(shù)據(jù),如果查詢得到多條數(shù)據(jù)會拋出TooManyResultsException
的異常。 - 比如用戶調(diào)用
SqlSession#selectMap
就是遍歷List中的每個元素,把這些元素轉(zhuǎn)換成key-value形式的Map結(jié)構(gòu)并返回 - 或者用戶自定義返回一個User對象,也會遍歷List,把元素轉(zhuǎn)換為指定類型的對象
mybatis中封裝了一個類叫做ResultSetHandler
它用來處理查詢數(shù)據(jù)庫得到的結(jié)果集,并把結(jié)果集解析為用戶指定類型的數(shù)據(jù)。它的調(diào)用時機就是在查詢玩數(shù)據(jù)庫之后,調(diào)用時機如下
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.execute(); return resultSetHandler.handleResultSets(ps); }
第一步先獲取PreparedStatement對象,第二部執(zhí)行execute方法查詢數(shù)據(jù)庫,第三步就是使用ResultSetHandler
處理結(jié)果集。接下來就來看下resultSetHandler是如何處理結(jié)果集對象的。
它的邏輯在ResultSetHandler#handleResultSets
方法中
ResultSetHandler#handleResultSets
ResultSetHandler是一個接口,它只有一個實現(xiàn)類DefaultResultSetHandler,下面是handleResultSets方法關(guān)的鍵代碼。我把此核心邏輯代碼分為了5部分,后面章節(jié)詳細介紹
public List<Object> handleResultSets(Statement stmt) throws SQLException { // 第一部分:用來緩存最后的返回值,每條記錄處理完之后都會存入該集合中 final List<Object> multipleResults = new ArrayList<>(); int resultSetCount = 0; /* * ResultSetWrapper對結(jié)果集(ResultSet)進行了包裝 * getFirstResultSet獲取了Statement中的第一個結(jié)果集對象,注:一般情況下只有一個結(jié)果集,如果調(diào)用存儲過程可能就會獲得多個結(jié)果集 */ ResultSetWrapper rsw = getFirstResultSet(stmt); // 第二部分 // 1. 先處理mappedStatement中的ResultMap標簽(每個XML的SQL語句都被映射成了MappedStatement對象。) // 每個SQL執(zhí)行的返回結(jié)果有可能是多個resultMap標簽共同組成的??赡苁嵌嘟Y(jié)果集 List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); // 第三部分 while (rsw != null && resultMapCount > resultSetCount) { // MappedStatement中的ResultMap數(shù)量應(yīng)該和 結(jié)果集的數(shù)量一致 ResultMap resultMap = resultMaps.get(resultSetCount); // 處理結(jié)果集,這是該方法中最重要的步驟 handleResultSet(rsw, resultMap, multipleResults, null); // 獲取下一個結(jié)果集(多結(jié)果集情況) rsw = getNextResultSet(stmt); // nestedResultObjects清空該對象,該對象是一個緩存 cleanUpAfterHandlingResultSet(); resultSetCount++; } // 第四部分 // 2. 先處理mappedStatement中的ResultSets標簽. 因為解析ResultMap的時候,可能ResultMap中包含ResultSet標簽,而ResultSet標簽并未解析 String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); } rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } } // 第五部分 return collapseSingleResultList(multipleResults); }
該代碼主要分為兩個大邏輯:
- 通過getFirstResultSet方法獲取第一個結(jié)果集對象,然后循環(huán)ps中的結(jié)果集,處理每個結(jié)果集。每個結(jié)果集處理完后的數(shù)據(jù)存放到multipleResults這個集合中
- 處理多結(jié)果集剩余的部分。因為用戶可能使用了resultSets標簽。返回2個結(jié)果集,但是在處理第一個結(jié)果集映射成用戶指定類型時,需要用到第二個結(jié)果集對象,這在第一步是無法完成的。只能在第二部完成。
比如有如下存儲函數(shù):getuserand_orders
create procedure get_user_and_orders(in id int) begin select * from user; select * from order; END;
該函數(shù)的業(yè)務(wù)意義是:查詢所有的用戶,和所有的訂單。在Mapper中定義的resultMap如下
<resultMap id="userMap" type="user"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="birthday" column="birthday"/> <result property="password" column="password"/> <association property="orderList" resultSet="orders"> <result property="name" column="name"/> </association> </resultMap> <!--resultSets的順序不能隨意放置,否則會導致結(jié)果集為空--> <select id="selectMoreResults2" statementType="CALLABLE" resultSets="users,orders" resultMap="userMap"> {call get_user_and_orders(1)} </select>
此時如果用戶執(zhí)行了存儲函數(shù),那么PS中的結(jié)果集 會有兩個,分別是users和orders。mybatis在處理結(jié)果集時發(fā)現(xiàn)。結(jié)果集中有兩個對象,先處理第一個,第一個結(jié)果集為users,自然要映射為User對象,給User對象的orderList屬性賦值時發(fā)現(xiàn)結(jié)果集中沒有關(guān)于訂單的數(shù)據(jù),因為訂單的數(shù)據(jù)在第二個結(jié)果集中。這時候就會在第二部再去處理第二個結(jié)果集。把訂單的結(jié)果集數(shù)據(jù)映射到User的orderList屬性中。
下面我們詳細分析上面這一長串代碼。
第一部分:ResultSetWrapper
首先我們來看第一部分的三行代碼
final List<Object> multipleResults = new ArrayList<>(); int resultSetCount = 0; ResultSetWrapper rsw = getFirstResultSet(stmt);
- 首先定義了一個List類型的集合multipleResults,結(jié)果集中每一條記錄解析完畢后的數(shù)據(jù)都會存放到該集合中
- 定義變量resultSetCount,它代表結(jié)果集的個數(shù)。(結(jié)果集的個數(shù)不一定等于ResultMaps的個數(shù)哦)
- 把結(jié)果集對象封裝為一個ResultSetWrapper對象。ResultSetWrapper其實就是對JDBC中的ResultSet對象做了一個封裝。包裝了一些元數(shù)據(jù)的信息。下面來看下ResultSetWrapper的重要結(jié)構(gòu)
public class ResultSetWrapper { private final ResultSet resultSet; private final TypeHandlerRegistry typeHandlerRegistry; // 結(jié)果集中的列名集合 private final List<String> columnNames = new ArrayList<>(); // java名稱集合 private final List<String> classNames = new ArrayList<>(); private final List<JdbcType> jdbcTypes = new ArrayList<>(); // ResultMap標簽中指定的映射(重要?。? private final Map<String, List<String>> mappedColumnNamesMap = new HashMap<>(); // ResultMap標簽中未指定的映射字段(重要!) private final Map<String, List<String>> unMappedColumnNamesMap = new HashMap<>(); }
- resultSet:JDBC中的結(jié)果集對象
- TypeHandlerRegistry:類型處理器,用于JDBC和Java類型的轉(zhuǎn)換
- columnNames:結(jié)果集中的所有列名的集合
- classNames:每一列對應(yīng)的Java類型的集合
- jdbcTypes:每一列對應(yīng)的JDBC類型的結(jié)合
- mappedColumnNamesMap:resultMap標簽中顯式定義的標簽Map
- unMappedColumnNamesMap:結(jié)果集中返回但resultMap標簽中未定義的列會被記錄在該Map中
第二部分:驗證rsw對象
上面獲取了rsw對象(ResultSetWrapper,后面簡稱rsw了)后,接下來需要驗證rsw對象。第二部分的三行代碼如下
List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount);
- 首先先從mappedStatement對象中獲取ResultMap對象,首先mappedStatement可以理解為XML中的每個
select|update|insert|delete
節(jié)點都被封裝成了MS對象(mappedStatement簡稱MS)。那么MS對象中其實就包含了每個select|update|insert|delete
節(jié)點的信息。而一個節(jié)點可能會在resultMap標簽上定義多個返回結(jié)果集。比如下面代碼在select標簽的resultMap屬性中定義了兩個結(jié)果集
<select id="selectMoreResults1" statementType="CALLABLE" resultMap="users,authors"> {call get_user_and_authors(1)} </select>
- 定義變量resultMapCount,它表示一個MS對象中resultMap的個數(shù)。通常它是1。我們常用的情況也是1。要注意,第一部分定義的resultSetCount變量和resultMapCount并不一定相等。比如PreparedStatement對象中有兩個結(jié)果集——那么此時的resultSetCount就是2.但是xml中select標簽的resultMap屬性只定義了一個映射——那么此時的resultMapCount就是1
- 當resultMapCount < resultSetCount的時候,就說明多個結(jié)果集對應(yīng)了XML中的一個映射關(guān)系,此時就需要解析resultSet標簽
- 最后一件事是驗證rsw是否合法,代碼比較簡單就不詳細介紹了
第三部分:遍歷rsw中的結(jié)果集
接下來就是要遍歷rsw中的結(jié)果集對象。并把結(jié)果集中的每條記錄都根據(jù)resultMap標簽定義的映射關(guān)系轉(zhuǎn)化為指定類型的數(shù)據(jù)。并把它加入到第一部分提到的multipleResults集合中。第三部分的代碼如下
while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); // 處理結(jié)果集,這是該方法中最重要的步驟 handleResultSet(rsw, resultMap, multipleResults, null); // 獲取下一個結(jié)果集(多結(jié)果集情況) rsw = getNextResultSet(stmt); // nestedResultObjects清空該對象,該對象是一個緩存 cleanUpAfterHandlingResultSet(); resultSetCount++; }
改代碼的意思是,當rsw存在并且resultMapCount > resultSetCount時
- 獲取結(jié)果集對應(yīng)的ResultMap對象
- 調(diào)用handleResultSet方法處理結(jié)果集對象(這個方法很重要,它實際上完成了結(jié)果集中的每條記錄的解析,它其中又調(diào)用了很多重要的方法,該方法后面我會單獨抽出一篇文章來講)
- 獲取下一個結(jié)果集并且空緩存對象。nestedResultObjects是解析嵌套映射中的一個緩存對象(了解即可)每次解析完一個結(jié)果集后都要清空該對象。
- 重復上述步驟。不過一般我們都是執(zhí)行單條SQL語句,所以PreparedStatement一般只有一個結(jié)果集,該循環(huán)也只會走一次。除非調(diào)用了存儲函數(shù)
第四部分:處理ResultSets標簽
如果在第一部分到第三部分的循環(huán)中,順序處理完結(jié)果集對象之后,resultSetCount數(shù)量還是大于resultMapCount,那么就證明PS對象返回的是多結(jié)果集,并且多結(jié)果集值對應(yīng)了一個映射關(guān)系,此時就需要解析這個ResultSets標簽。它的解析流程和第三部分一樣,重點就在于handleResultSet
方法。下面使用一個案例來詳細說明為什么會有這部分的解析。
- 定義一個存儲函數(shù)
create procedure get_user_and_orders(in id int) begin select * from user; select * from order; END;
- xml中配置調(diào)用存儲函數(shù)的select節(jié)點
<resultMap id="userMap" type="user"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="birthday" column="birthday"/> <result property="password" column="password"/> <association property="orderList" resultSet="orders"> <result property="name" column="name"/> </association> </resultMap> <!--resultSets的順序不能隨意放置,否則會導致結(jié)果集為空--> <select id="selectMoreResults2" statementType="CALLABLE" resultSets="users,orders" resultMap="userMap"> {call get_user_and_orders(1)} </select>
- 用戶調(diào)用selectMoreResults2這個方法。很顯然selectMoreResults2的返回結(jié)果就是存儲函數(shù)執(zhí)行的結(jié)果,它執(zhí)行了兩個select語句,意味著會生成兩個結(jié)果集對象,xml中select標簽定義該存儲函數(shù)的執(zhí)行結(jié)果值對應(yīng)一個映射關(guān)系就是userMap。但是兩個結(jié)果集怎么映射成一個resultMap呢?我們真正想要的結(jié)果是把第二個結(jié)果集映射到userMap中的orderList屬性。所以在進行第三部分進行遍歷的時候,循環(huán)只會走一次,因為resultSetCount=2,resultMapCount=1,讀者可以自定使用該業(yè)務(wù)代碼進行斷點調(diào)試。在解析第一個結(jié)果集時發(fā)現(xiàn)第一個結(jié)果集中沒有orderList的信息。無法完成映射。所以才會走到第四部分進行結(jié)果集映射!
第五部分:collapseSingleResultList
最后一部分很簡單,它只是把最后返回的結(jié)果進行判斷:如果返回結(jié)果multipleResults集合大小為1,則只返回集合中的這個元素,否則返回原對象本身
private List<Object> collapseSingleResultList(List<Object> multipleResults) { return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults; }
總結(jié)
該篇講述了mybatis在執(zhí)行完數(shù)據(jù)庫后進行結(jié)果集的大致解析過程。
- ResultSetWrapper是對JDBC中的ResultSet對象的封裝
- 結(jié)果集解析的重點在
DefaultResultSetHandler#handleResultSet
這個方法中 - XML中的resultMap可以定義多個映射關(guān)系。如果多個結(jié)果集對應(yīng)一個映射關(guān)系就需要第四部分(對resultSets標簽的處理)
后續(xù)我會帶來handleResultSet
方法的解析~
更多關(guān)于MyBatis ResultSetHandler結(jié)果集的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
windows下使用 intellij idea 編譯 kafka 源碼環(huán)境
這篇文章主要介紹了使用 intellij idea 編譯 kafka 源碼的環(huán)境,本文是基于windows下做的項目演示,需要的朋友可以參考下2021-10-10SpringBoot啟動多數(shù)據(jù)源找不到合適的驅(qū)動類問題
這篇文章主要介紹了SpringBoot啟動多數(shù)據(jù)源找不到合適的驅(qū)動類問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01Java經(jīng)典設(shè)計模式之責任鏈模式原理與用法詳解
這篇文章主要介紹了Java經(jīng)典設(shè)計模式之責任鏈模式,簡單說明了責任鏈模式的概念、原理,并結(jié)合實例形式分析了java實現(xiàn)責任鏈模式的具體用法與相關(guān)注意事項,需要的朋友可以參考下2017-08-08使用Springboot 打jar包實現(xiàn)分離依賴lib和配置
這篇文章主要介紹了使用Springboot 打jar包實現(xiàn)分離依賴lib和配置方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02SpringMVC跨服務(wù)器上傳文件中出現(xiàn)405錯誤的解決
這篇文章主要介紹了SpringMVC跨服務(wù)器上傳文件中出現(xiàn)405錯誤的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09