MyBatis使用Collection查詢多對多或一對多結果集bug


情況描述:當使用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結果

    


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM