MyBatis使用Collection查询多对多或一对多结果集bug

情况描述: 当使用 JOIN 查询, 如果 SQL 查询出来的记录不是按 id 列排序的, 则生成的 List 结果会有问题

案例:

1) 数据库模型

简而言之一个 Goods 包含多个 Goods_Img

 

2) Java Bean

Goods.java

public class Goods {
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Integer goodsId;

</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String goodsName;

</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Integer goodsStorageNum;

</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Integer goodsScore;

</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> GoodsStatus goodsStatus;

</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String goodsDescription;

</span><strong><span style="color: rgba(0, 0, 255, 1)">private</span> List&lt;GoodsImg&gt;</strong><span style="color: rgba(0, 0, 0, 1)"><strong> goodsImgList;</strong>

... getter and setter ...

}

GoodImg.java

public class GoodsImg {
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Integer imgId;

</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Integer goodsId;

</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String imgDir;

</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Integer imgSize;

</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> 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>
<span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)"> goodsimage resultmap </span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">resultMap </span><span style="color: rgba(255, 0, 0, 1)">id</span><span style="color: rgba(0, 0, 255, 1)">="goodsImgResult"</span><span style="color: rgba(255, 0, 0, 1)"> type</span><span style="color: rgba(0, 0, 255, 1)">="com.qunar.scoresystem.bean.GoodsImg"</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
    <span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">id </span><span style="color: rgba(255, 0, 0, 1)">property</span><span style="color: rgba(0, 0, 255, 1)">="imgId"</span><span style="color: rgba(255, 0, 0, 1)"> column</span><span style="color: rgba(0, 0, 255, 1)">="img_id"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span>
    <span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">result </span><span style="color: rgba(255, 0, 0, 1)">property</span><span style="color: rgba(0, 0, 255, 1)">="goodsId"</span><span style="color: rgba(255, 0, 0, 1)"> column</span><span style="color: rgba(0, 0, 255, 1)">="goods_id"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span>
    <span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">result </span><span style="color: rgba(255, 0, 0, 1)">property</span><span style="color: rgba(0, 0, 255, 1)">="imgDir"</span><span style="color: rgba(255, 0, 0, 1)"> column</span><span style="color: rgba(0, 0, 255, 1)">="img_dir"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span>
    <span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">result </span><span style="color: rgba(255, 0, 0, 1)">property</span><span style="color: rgba(0, 0, 255, 1)">="imgSize"</span><span style="color: rgba(255, 0, 0, 1)"> column</span><span style="color: rgba(0, 0, 255, 1)">="img_size"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span>
    <span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">result </span><span style="color: rgba(255, 0, 0, 1)">property</span><span style="color: rgba(0, 0, 255, 1)">="imgName"</span><span style="color: rgba(255, 0, 0, 1)"> column</span><span style="color: rgba(0, 0, 255, 1)">="img_name"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">resultMap</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span></pre>

 

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 结果