0x00 背景
一个历史悠久的项目,使用的技术栈主要是 spring cloud 体系,属于 service 范畴,不给外部提供接口,但是集成了 myabtis-pagehelper,具体的版本如下:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
相关的配置:
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
0x01 现象描述
同事在这个项目中新增加了一个总能,属于定时执行的,循环取数据,每次取 1000 条,但是上线后发现第一次能正常查询出数据,但是第二次往后就查询不出数据了,但是数据库中是有数据的。SQL 如下:
-- sql 已经做简化处理
select
*
from ${tableName}
where type = #{type,jdbcType=VARCHAR}
and time >= #{time,jdbcType=TIMESTAMP}
<if test="pageSize != null and pageSize != '' and pageNum!=null and pageNum!=''">
limit #{pageNum}, #{pageSize}
</if>
0x02 问题分析
刚刚开始以为是 sql 和数据的问题,一顿分析,将 sql 的参数参数打印出来后,用工具查询发现是能查询出数据的。由此判断不是参数以及sql 的问题,于是添加 mybatis 的参数将 sql 打印出来。
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
查看查询的过程, 发现每一次查询会先执行一个 count 操作,查询总数,总数查询完成后,再去执行查询操作。第一次 count 是有数据,但是第二次查询 count 查询的总数为 0,导致也就不执行查询操作了。看完查询过程后,同事说他这个不是分页,我有个疑问为啥会执行分页操作呢,但是也没多想。继续排查对比。
经过对比前后两次的 count 的 sql 发现,第二次的 count 的 sql 多了一个 limit,而此时的 pageNum 和 pageSize 变成了 1,1。 count 的结果总列数就是 1,limit 1, 1 后肯定是没有结果的。
0x03 问题解决
发现这个情况后,前面说过他应该不走分页逻辑,应该是正常自定义 limit,也就是说不应该会前置执行 count 计数查询,而应该直接去查询数据。猜想是 pageNum 和 pageSize 两个参数导致的,将参数改名后发现正常执行查询而不执行 count 操作了。
那么究竟是为啥会这样呢,经过查看源码,发现在 PageParams 中有如下的逻辑:
} else if (parameterObject instanceof IPage || supportMethodsArguments) {
try {
page = PageObjectUtil.getPageFromObject(parameterObject, false);
} catch (Exception e) {
return null;
}
}
如果参数实现了 IPage 接口或者 supportMethodsArguments 为 true 就会从参数中获取 pageNum 和 pageSize 参数,也就是当 supportMethodsArguments 为 true 时并且参数中有 pageNum 和 pageSize 参数时,会执行分页的相关逻辑。
0x04 总结
总的来说,supportMethodsArguments 为 true 时需要特别注意,传递的参数中不能有 pageNum 和 pageSize 参数,否则会执行分页逻辑。
带着好奇心去看了一下 pagehepler 的源码,发现某些特殊的 sql 走分页逻辑时,会重写成子查询,比如:
select so.id,so.address,so.area_code,so.area_id,so.del_flag,so.email,so.fax,so.grade,so.icon,so.master, so.name,so.parent_id,so.parent_ids,so.phone,so.remarks,so.type,so.zip_code from sys_organization so LEFT JOIN sys_user_organization suo ON (suo.org_id = so.id or FIND_IN_SET(suo.org_id,so.parent_ids)) where suo.user_id = ? group by so.id LIMIT ? "
生成的 count 语句是:
SELECT count(0) FROM (SELECT so.id, so.address, so.area_code, so.area_id, so.del_flag, so.email, so.fax, so.grade, so.icon, so.master, so.name, so.parent_id, so.parent_ids, so.phone, so.remarks, so.type, so.zip_code FROM sys_organization so LEFT JOIN sys_user_organization suo ON (suo.org_id = so.id OR FIND_IN_SET(suo.org_id, so.parent_ids)) WHERE suo.user_id = ? GROUP BY so.id LIMIT ?) table_count
查看了一下源码是有特殊的判断,我们遇到的那个 sql 不满条件,所以只是简单的重写了 sql,没有重写成子查询。
因为是 count 语句的话其实不应该有 limit,有 limit 的话多多少少有点问题。也尝试修复了一下,虽然能重写成子查询,虽然查询 count 正常了,后续正常的查询就不正常(需要处理 sql 的参数,看逻辑后续会还拼接 limit 导致语法错误,想想也没有啥意义(虽然带了 limit但是可能也不是分页),所以还是放弃了。后续关注下,看看作者大大有没有更好的解决办法。