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<GoodsImg></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)"><!--</span><span style="color: rgba(0, 128, 0, 1)"> goodsimage resultmap </span><span style="color: rgba(0, 128, 0, 1)">--></span> <span style="color: rgba(0, 0, 255, 1)"><</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)">></span> <span style="color: rgba(0, 0, 255, 1)"><</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)">/></span> <span style="color: rgba(0, 0, 255, 1)"><</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)">/></span> <span style="color: rgba(0, 0, 255, 1)"><</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)">/></span> <span style="color: rgba(0, 0, 255, 1)"><</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)">/></span> <span style="color: rgba(0, 0, 255, 1)"><</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)">/></span> <span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">resultMap</span><span style="color: rgba(0, 0, 255, 1)">></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 结果