情況描述:當使用JOIN查詢,如果SQL查詢出來的記錄不是按id列排序的,則生成的List結果會有問題
案例:
1) 數據庫模型
簡而言之一個Goods包含多個Goods_Img
2) Java Bean
Goods.java
public class Goods { private Integer goodsId; private String goodsName; private Integer goodsStorageNum; private Integer goodsScore; private GoodsStatus goodsStatus; private String goodsDescription; private List<GoodsImg> goodsImgList; ... getter and setter ... }
GoodImg.java
public class GoodsImg { private Integer imgId; private Integer goodsId; private String imgDir; private Integer imgSize; private String imgName; ... getter and setter ... }
3) Mapper
<!-- Result Map --> <!-- goods resultmap --> <resultMap id="goodsResultMap" type="com.qunar.scoresystem.bean.Goods"> <id property="goodsId" column="goods_id" /> <result property="goodsName" column="goods_name" /> <result property="goodsStorageNum" column="goods_storage_num" /> <result property="goodsScore" column="goods_score" /> <result property="goodsDescription" column="goods_description" /> <result property="goodsStatus" column="goods_status" /> <collection property="goodsImgList" resultMap="goodsImgResult" /> </resultMap> <!-- goodsimage resultmap --> <resultMap id="goodsImgResult" type="com.qunar.scoresystem.bean.GoodsImg"> <id property="imgId" column="img_id" /> <result property="goodsId" column="goods_id" /> <result property="imgDir" column="img_dir" /> <result property="imgSize" column="img_size" /> <result property="imgName" column="img_name" /> </resultMap>
4) 執行的 SQL
select goods.goods_id as goods_id, goods.goods_name as goods_name, goods.goods_storage_num as goods_storage_num, goods.goods_score as goods_score, goods.goods_description as goods_description, goods.goods_status as goods_status , goods_img.img_name as img_name , goods_img.img_dir as img_dir , goods_img.img_size as img_size from goods join goods_img on goods.goods_id=goods_img.goods_id
5) 結果集
a. 當SQL查詢的結果為
注意上圖中的goods_id順序為亂序
則MyBatis返回的List結果為
Goods{goodsId=1, goodsName='good1', goodsStorageNum=1, goodsScore=1, goodsStatus=[1 | 下架], goodsDescription='1', goodsImgList=[GoodsImg{imgId=null, goodsId=1, imgDir='d1', imgSize=1, imgName='img1'}, GoodsImg{imgId=null, goodsId=1, imgDir='d2', imgSize=2, imgName='img2'}, GoodsImg{imgId=null, goodsId=1, imgDir='d4', imgSize=4, imgName='img4'}, GoodsImg{imgId=null, goodsId=1, imgDir='d6', imgSize=6, imgName='img6'}]}
Goods{goodsId=1, goodsName='good1', goodsStorageNum=1, goodsScore=1, goodsStatus=[1 | 下架], goodsDescription='1', goodsImgList=[GoodsImg{imgId=null, goodsId=1, imgDir='d1', imgSize=1, imgName='img1'}, GoodsImg{imgId=null, goodsId=1, imgDir='d2', imgSize=2, imgName='img2'}, GoodsImg{imgId=null, goodsId=1, imgDir='d4', imgSize=4, imgName='img4'}, GoodsImg{imgId=null, goodsId=1, imgDir='d6', imgSize=6, imgName='img6'}]}
Goods{goodsId=1, goodsName='good1', goodsStorageNum=1, goodsScore=1, goodsStatus=[1 | 下架], goodsDescription='1', goodsImgList=[GoodsImg{imgId=null, goodsId=1, imgDir='d1', imgSize=1, imgName='img1'}, GoodsImg{imgId=null, goodsId=1, imgDir='d2', imgSize=2, imgName='img2'}, GoodsImg{imgId=null, goodsId=1, imgDir='d4', imgSize=4, imgName='img4'}, GoodsImg{imgId=null, goodsId=1, imgDir='d6', imgSize=6, imgName='img6'}]}
可見返回的結果中有 三個 一模一樣的 Goods(id=1,且包含5個GoodsImg), 而我們期待的結果應該是 List{ Goods(id=1), Goods(id=2), Goods(id=3) }
b. 當使用的SQL查詢結果如下
上面的查詢結果為id有序結果,正則MyBatis返回的Java結果集為:
Goods{goodsId=1, goodsName='good1', goodsStorageNum=1, goodsScore=1, goodsStatus=[1 | 下架], goodsDescription='1', goodsImgList=[GoodsImg{imgId=null, goodsId=1, imgDir='d1', imgSize=1, imgName='img1'}, GoodsImg{imgId=null, goodsId=1, imgDir='d2', imgSize=2, imgName='img2'}, GoodsImg{imgId=null, goodsId=1, imgDir='d3', imgSize=3, imgName='img3'}, GoodsImg{imgId=null, goodsId=1, imgDir='d4', imgSize=4, imgName='img4'}]}
Goods{goodsId=2, goodsName='good2', goodsStorageNum=2, goodsScore=2, goodsStatus=[1 | 下架], goodsDescription='2', goodsImgList=[GoodsImg{imgId=null, goodsId=2, imgDir='d5', imgSize=5, imgName='img5'}]}
Goods{goodsId=3, goodsName='good3', goodsStorageNum=3, goodsScore=3, goodsStatus=[1 | 下架], goodsDescription='3', goodsImgList=[GoodsImg{imgId=null, goodsId=3, imgDir='d6', imgSize=6, imgName='img6'}]}
觀察goodsId,我們取得了期待的結果
答案:
根據作者本人的解釋, MyBatis為了降低內存開銷,采用ResultHandler逐行讀取的JDBC ResultSet結果集的,這就會造成MyBatis在結果行返回的時候無法判斷以后的是否還會有這個id的行返回,所以它采用了一個方法來判斷當前id的結果行是否已經讀取完成,從而將其加入結果集List,這個方法是:
1. 讀取當前行記錄A,將A加入自定義Cache類,同時讀取下一行記錄B
2. 使用下一行記錄B的id列和值為key(這個key由resultMap的<id>標簽列定義)去Cache類里獲取記錄
3. 假如使用B的key不能夠獲取到記錄,則說明B的id與A不同,那么A將被加入到List
4. 假如使用B的key可以獲取到記錄,說明A與B的id相同,則會將A與B合並(相當於將兩個goodsImg合並到一個List中,而goods本身並不會增加)
5. 將B定為當前行,同時讀取下一行C,重復1-5,直到沒有下一行記錄
6. 當沒有下一行記錄的時候,將最后一個合並的resultMap對應的java對象加入到List(最后一個被合並goodsImg的Goods)
所以
a. 當結果行是亂序的,例如BBAB這樣的順序,在記錄行A遇到一個id不同的曾經出現過的記錄行B時, A將不會被加入到List里(因為Cache里已經存在B的id為key的cahce了)
b. 當結果是順序時,則結果集不會有任何問題,因為 記錄行 A 不可能 遇到一個曾經出現過的 記錄行B, 所以記錄行A不會被忽略,每次遇到新行B時,都不可能使用B的key去Cache里取到值,所以A必然可以被加入到List
在MyBatis中,實現這個邏輯的代碼如下
@Override protected void handleRowValues(ResultSet rs, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultColumnCache resultColumnCache) throws SQLException { final DefaultResultContext resultContext = new DefaultResultContext(); skipRows(rs, rowBounds); Object rowValue = null; while (shouldProcessMoreRows(rs, resultContext, rowBounds)) { final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rs, resultMap, null); // 下一記錄行的id構成的cache key final CacheKey rowKey = createRowKey(discriminatedResultMap, rs, null, resultColumnCache); Object partialObject = objectCache.get(rowKey); // 判斷下一記錄行是否被記錄與cache中,如果不在cache中則將該記錄行的對象插入List if (partialObject == null && rowValue != null) { // issue #542 delay calling ResultHandler until object if (mappedStatement.isResultOrdered()) objectCache.clear(); // issue #577 clear memory if ordered callResultHandler(resultHandler, resultContext, rowValue); } // 當前記錄行的值 rowValue = getRowValue(rs, discriminatedResultMap, rowKey, rowKey, null, resultColumnCache, partialObject); } // 插入最后一記錄行的對象到List if (rowValue != null) callResultHandler(resultHandler, resultContext, rowValue); }
舉例:
以
這個結果集為例,MyBatis會逐行讀取記錄行,我們將1~6行編號為A,B,C,D,E,F
1. 讀取A行(id=1),將A行加入Cache,查看B行(id=1)的id,B行在Cache中已存在,不操作
2. 讀取B行(id=1),查看C(id=2)行id,C行在Cache中不存在,將B行對應的Java對象插入List
3. 讀取C(id=2)行,查看D(id=1)行ID,D行在Cache中已存在,不操作(此處漏掉一個id=2的Goods)
4. 讀取D行(id=1),查看E行(id=3)ID,E行在Cache中不存在,將D行對應的java對象插入List(此處插入第一個重復的id=1的Goods)
5. 讀取E行(id=3),查看F行(id=1)的ID,F行在Cache中已存在,不操作(此處漏掉一個id=3的Goods)
6. 讀取F行(id=1),沒有下一行,跳出循環,並插入最后一個Goods(此處插入第二個重復id=1的Goods)
所以,最后我們得到了3個一樣的Goods,至於有序結果集,大家也可以按順序去推一下,得到的結果集就是正確的
此外,源碼中我們也可以看到作者原先給的注釋: issue #542,討論的就是這個問題,參見如下鏈接
https://github.com/mybatis/mybatis-3/pull/22
https://code.google.com/p/mybatis/issues/detail?id=542
所以,如果我們要用這種方式去查詢一對多關系,恐怕只能手動排序好結果集才不會出錯.
另外,我還發現一個有趣的現象,就是當MySQL在主表數據量<=3條時,Join的結果集是無序的,而當結果集的數據量>3條時,Join的結果集就變成有序了
a. 主表數據<=3條
主表:
Join結果
b. 主表數據>3行
主表
Join結果