Mybatis查询列表中的坑

前言

从一个Bug入手,看一个或许被很多人忽略的Mybatis使用中的大坑。
中间是排查思路。如果不想看排查过程,可以直接从[真正的解决方案]开始看。

Bug描述

JavaWeb项目中,使用Mybatis查询pg数据库。
在查询一个列表数据的时候,发现该列表的倒数第二页,缺了一条数据(pageSize=100,结果那一页只有99条)
把控制台打印的sql和参数日志拿出来,去数据库查,发现是可以查出100条的。甚至控制台上显示的sql日志也显示查出了100条。
看了一下sql,发现用了row_number()分页查询,类似于:

<select id="list_data", resultMap = "test_resultmap">
select * from (
	select row_number() row_num, t.*
	from (select a.*, b.* from A a left join B b on a.id=b.aid order by a.sort_field desc) as t
)  as t1
where row_num >= #{start}  and row_number <= #{end}
</select>

通过对比Mybatis查询的数据数据库执行相同sql得到的数据。发现表B中有部分数据出现了重复。
实际业务中,B表不应该有数据重复,而是应该和A表一一对应才对。
通过删除B表中的脏数据,再次验证,这个问题就算解决了。

站在业务角度上讲,这个问题就算解决了。
可站在技术角度讲,只是这个业务场景中B表不允许重复。可两表之间是一对多的关系,是很常见的设计。

排查

分页Total查询条件和List数据查询条件不一致

排查后发现Total的统计没问题。

row_number分页语句的问题

参考文章:使用 ROW_NUMBER () 排序后分页查询的坑
发现换成Limit查询,会出现同样的问题。
这个似乎很靠谱,或许逻辑是这样的:
因为B表重复,导致整个查询结果重复,排序字段又是A表的字段,所以那些重复的数据的排序字段值也是重复的——>排序不稳定 ——>同一个数据出现在第2也,也出现在第3页。当出现在第3页的时候,因为Mybatis的某种机制,这条奇怪的数据就被发现了,然后就被Mybatis过滤掉了。
试试网上给出的方案,给排序条件增加了一个排序字段。倒数第二页真的变成100条了。
问题解决了?
: 属于瞎猫碰见死耗子。其实根本就不是同一个问题。而且这个所谓的“解决方案”,还引起了排序混乱。
或者说是“正是因为排序被打乱了,所以之前那个错误的页跑到其他地方去了,只是倒数第二页变正常了而已”。然后就被误解为正确解释和方案了。

多字段排序

前面说:用了一个错误的方案居然“解决”了问题,实际上只是因为排序打乱了而已。为什么排序会被打乱? 是因为理解错了两个字段排序的写法:
order by a, b desc
等效于 order by a asc, b desc
而非等效于 order by a desc, b desc
也就是每个字段都要紧跟自己的排序方式,如果不写,则默认为asc

看源码

这部分放到另一篇博客里。

真正的解决方案

问题的真正根源其实在Mapper.xml的 ResultMapper中。也就是前面那个select语句中的resultMap = "test_resultmap",类似如下所示(业务代码中实际很复杂,这里就抽象出主要特征)

<resultMap type="com.test.Abc" id="test_resultmap">
        <id property="aId" column="id"/>
        <result property="aName" column="aname"/>
        <result property="aOrderField" column="aorderfield"/>
        <result property="bid" column="bid"/>
        <result property="bName" column="bname"/>
        <result property="bOrderField" column="borderfield"/>
        <collection property="otherInfo" javaType="ArrayList" ofType="com.test.C">
        	<result property="cid" column="id"/>
        	<result property="cName" column="cname"/>
        </collection>
    </resultMap>`

这里的问题点就在于,多了一个嵌套类otherInfo,可select语句又没有真正的用上。

解决方案就是把这个多余的嵌套类配置删掉(实际我是另外重写了一个ResultMap,因为原来那个有其他select在引用),问题就解决了。

解释

Mybatis执行完sql拿到数据后,需要对数据进行封装。在上面这个例子中,就是把数据封装进
com.test.Abc对象中。
封装数据的时候,分两种情况:简单ResultMap嵌套ResultMap

  • 简单ResultMap:数据库返回几条就封装几个
  • 嵌套ResultMap:对返回的数据进行去重,然后封装返回

上面的示例Bug,一开始就是把ResultMap写成了嵌套类型,但实际上自己的返回值里根本没有嵌套类型的数据。
可Mybatis没那么智能,它发现ResultMap是嵌套类,就不管三七二十一的对数据进行了去重操作。可写这个方法人并没有这种去重意图。Bug因此就产生了。
大概就是写这个查询方法的人看到前面这个ResultMap里的字段包含了自己业务所需要的所有字段,所以就想着这个ResultMap是可以通用的,就直接引用在自己的select方法里了

另一个坑

现在假设查询sql返回的有嵌套类型,并且确实希望它嵌套在Abc里

<select id="list_data", resultMap = "test_resultmap">
select * from (
	select row_number() row_num, t.*
	from (
		select a.*, b.*, c.*
		from A a 
		left join B b on a.id=b.aid
		left join C c on a.id=c.aid
		order by a.sort_field desc) as t
)  as t1
where row_num >= #{startIndex}  and row_number <= #{endIndex}
</select>

com.test.Abc如下

public class Abc {
	private int aId;
	private String aName;
	
	private int bId;
	private String bName;
	
	private C otherInfo;//嵌套类型
	...
}

此时就符合业务逻辑了吗?
:并没有。查询结果依然是根据A表去重,而我们实际希望的是根据A和B共同判断唯一条件。
问题出在<id property="aId" column="id"/>
当使用嵌套类型ResultMap时,Mybatis就会对结果集进行去重。
去重的标准:

  • id元素标签,就以此标签指定的字段进行去重
  • 没有id元素标签,就以整个ResultMap中所有字段拼起来作为去重标准(因此Mybatis官网和很多博客中,都会说这么一句话:id和result的唯一区别是id将结果标记为标识符属性,以便在比较对象实例时使用,这有助于提升性能

所查即所得?

mybatis在很多人的潜意识里,就是所查即所得(相比hibernate的高度封装,我们要的就是mybatis轻量级的自由)。
所以在这个问题的排查中,当我发现所有的线索都指向了:Mybatis对数据进行了去重操作
我就陷入了深深的自我怀疑(写了这么多年crud,竟然不知道Mybatis还有这种操作,Mybatis怎么会做这么“自作主张”的事呢)
实际上,Mybatis还真有一个合情合理的去重场景:

当我们要查询”一对多“的场景时,可以使用嵌套类型。可以直接获得一个类似于

// A和B是一对多关系
class A {
private int aId;
private String aName;
private List<B> bList;//嵌套B集合
}

这样的封装好的结果集。这样的结果是怎么来的呢。
简单想想就会明白:从数据库查出来的数据,肯定是类似于这样的结果:
aId1, aName1, bId1, bName1
aId1, aName1, bId2, bName2
aId2, aName2, bId3, bName3
aId2, aName2, bId4, bName4
要把这样的结果封装到A对象集合中,必然要对返回值中A表的数据进行去重。然后再把B表的字段值分别封装到A对象中

之所以一时联想不到“一对多”的场景,除了“去重”和“一对多”看起来确实相关性不强,更重要的我觉得还是这种场景应用的比较少的缘故。
工作过程中,虽然sql越写越复杂,可我们总是倾向于用复杂的sql,简单的结果集。 在潜意识里总觉得,用复杂的数据结构的 都是初学者的demo。因为结果类型越复杂,通用性越差,引起不必要问题的可能性就越大(事实上也确实如此)。可尽量不用,却不能不懂其原理。

引申

根据上面所说的,我们知道是Mybatis处理“一对多”时,自动对结果去重导致的问题。
那么再问一个问题:如果Mapper.xml里ResultMap用了嵌套类型,但结果类型确实这么写的
resultType = "java.util.Map"
Mybatis还会对结果进行去重吗?
答:不会。
因为Mybatis需要实体类和Mapper.xml里ResultMap同时是嵌套类型,才会进行去重。

Mybatis分页查询数据常见问题

问题描述 原因 解决方案
Mybatis查询列表数据量和sql执行的数据量不一致【本文的主题】 执行了嵌套类的ResultMap【Mybatis结果集处理机制问题】 把乱套用的嵌套类ResultMap处理一下
Mybatis查询列表数据的嵌套类型集合中只有一条数据(比如,订单表和商品表一对多,结果查出来商品集合里永远只有一条数据) 主表的id名称和嵌套类的id名称重复了【Mybatis结果集处理机制问题】 用别名区分一下
同一条数据出现在多个分页中 列表查询排序字段重复了【sql问题,和Mybatis无关】 增加一个排序字段
使用PageHelper引起的分页问题 一般都是Mybatis框架直接简单粗暴套分页语句引起的。比如直接在你的sql后面增加limit语句(这一般很容易通过sql日志发现问题)【Mybatis处理sql语句问题】 自己写分页,不要用PageHelper(有些博客说使用Mybatis的懒加载,也可以。但不建议,复杂的sql还是自己控制比较好)

参考

使用 ROW_NUMBER () 排序后分页查询的坑
resultmap中点id resultmap的id属性
Mybatis官网文档
MyBatis查询List返回数据只有少部分,因为结果去重了
Mybatis中用法(主要用于一对多去重)

相关推荐

  1. Mybatis查询列表

    2024-03-21 13:30:05       44 阅读
  2. Mybatis如何将多个查询结果封装为一个对象列表

    2024-03-21 13:30:05       39 阅读
  3. 项目当中使用JPA、Hibernate、MyBatis遇到

    2024-03-21 13:30:05       54 阅读
  4. 记录一点mybatis

    2024-03-21 13:30:05       21 阅读
  5. MyBatis延迟加载与分步查询总结

    2024-03-21 13:30:05       31 阅读
  6. MyBatisif判断(踩

    2024-03-21 13:30:05       30 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-21 13:30:05       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-21 13:30:05       100 阅读
  3. 在Django里面运行非项目文件

    2024-03-21 13:30:05       82 阅读
  4. Python语言-面向对象

    2024-03-21 13:30:05       91 阅读

热门阅读

  1. linux查看/修改各种资源限制ulimit

    2024-03-21 13:30:05       39 阅读
  2. Golang 环境变量配置 mockgen安装(macOS系统)

    2024-03-21 13:30:05       42 阅读
  3. SVM支持向量机

    2024-03-21 13:30:05       41 阅读
  4. 数据结构奇妙旅程之红黑树

    2024-03-21 13:30:05       48 阅读
  5. ElasticSearch - 基础概念和映射

    2024-03-21 13:30:05       38 阅读
  6. 【逆向】fridaAPI_如何hook一个静态方法和实例方法

    2024-03-21 13:30:05       47 阅读
  7. 后端异常处理:全局异常处理器

    2024-03-21 13:30:05       47 阅读
  8. 亚信安慧AntDB全景观察:数据库领域的创新者

    2024-03-21 13:30:05       41 阅读
  9. FPGA_AD9361

    2024-03-21 13:30:05       43 阅读
  10. 力扣126双周赛

    2024-03-21 13:30:05       45 阅读
  11. electron-builder打包

    2024-03-21 13:30:05       46 阅读