Mybatis查询列表中的坑

前言

从一个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

看Mybatis源码

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

真正的解决方案

问题的真正根源其实在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将结果标记为标识符属性,以便在比较对象实例时使用,这有助于提升性能

其实这个id标签的作用很重要,是真的可以提升性能(通过一个字段去重和通过一堆字段去重,效率是完全不同的)。

如果业务中真的需要Mybatis根据多张表的数据进行去重操作。尽量给这个实体类增加一个"虚拟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对象中的bList中。

之所以一时联想不到"一对多"的场景。除了"去重"和"一对多"看起来确实相关性不强,更重要的我觉得还是这种场景应用的比较少的缘故。

工作过程中,虽然sql越写越复杂,可我们总是倾向于用复杂的sql,简单的结果集。 在潜意识里总觉得,用复杂的数据结构的 都是初学者的demo。因为结果类型越复杂,通用性越差,引起不必要问题的可能性就越大(事实上也确实如此)。可尽量不用,却不能不懂其原理,否则遇到像这样的bug就可能不知所措。

引申

根据上面所说的,我们知道是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中用法(主要用于一对多去重)

相关推荐
郑祎亦2 小时前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis
jokerest12316 小时前
web——sqliabs靶场——第十三关——报错注入+布尔盲注
mybatis
武子康16 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
WindFutrue19 小时前
使用Mybatis向Mysql中的插入Point类型的数据全方位解析
数据库·mysql·mybatis
AiFlutter20 小时前
Java实现简单的搜索引擎
java·搜索引擎·mybatis
天天扭码1 天前
五天SpringCloud计划——DAY1之mybatis-plus的使用
java·spring cloud·mybatis
武子康2 天前
Java-05 深入浅出 MyBatis - 配置深入 动态 SQL 参数、循环、片段
java·sql·设计模式·架构·mybatis·代理模式
2的n次方_2 天前
MyBatis——#{} 和 ${} 的区别和动态 SQL
数据库·sql·mybatis
jokerest1232 天前
web——sqliabs靶场——第十二关——(基于错误的双引号 POST 型字符型变形的注入)
数据库·sql·mybatis