1、bug描述

以前做分页查询时,我都是直接将page对象扔到sql查询的方法中,mybatis-plus会自动给page进行赋值。然而在这次问题中,返回的total结果中有数据,但是recodes却没有数据

image-20240221174920830

2、bug复现

伪代码如下:

service层代码

1
2
3
4
5
public Page<User> queryList() {
Page<User> page = new Page<>(1, 10);
List<User> userList = userMapper.queryList(page);
return page;
}

mapper层代码

1
List<User> queryList(@Param("page") Page<User> page);

xml查询语句

1
2
3
<select id="queryList" resultType="com.example.entity.User">
select * from user
</select>

最后可以看到page对象的数据:

image-20240223113801168

3、定位问题

既然以前是可以在查询sql时给page进行赋值recodes的,那我们就追一下源码,看mybatis-plus是在哪里发起的sql

追源码时,建议将源码下载来,idea中可以直接下,例如:

image-20240223114317398
不然的话,打断点执行时会有偏差,或者运行不到断点

当我们进入到发sql的断点时,点击进入方法内

image-20240223115056086

点几次进入方法内后,我们会进入到com.baomidou.mybatisplus.core.override包下的MybatisMapperMethod#execute方法,这里就是执行SQL的地方

image-20240223115347588

很明显这里分了增删改查,我们的查询,所以这里直接在查询那打断点,直接进入断点

image-20240223115632875

可以看到,这里是根据method.returnsXXX来判断执行的哪个方法,我们分别看一下里面的方法。当看到result = executeForIPage(sqlSession, args);时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private <E> Object executeForIPage(SqlSession sqlSession, Object[] args) {
IPage<E> result = null;
for (Object arg : args) {
if (arg instanceof IPage) {
result = (IPage<E>) arg;
break;
}
}
Assert.notNull(result, "can't found IPage for args!");
Object param = method.convertArgsToSqlCommandParam(args);
List<E> list = sqlSession.selectList(command.getName(), param);
result.setRecords(list);
return result;
}

这里面有很明显的setRecords方法,而result其实就是args传参的page对象。所以这大概率就是要找的自动设置records地方了,而其他的方法都没有这个

所以,如何进入这个方法呢?

上面说过,根据method.returnsXXX来判断执行的哪个方法,而从判断来看,可以怀疑这是根据返回值的类型来判断的。这时候的method为:

image-20240223120930997

可以看到reurnType是和查询的sql返回值类型是一样的。因此,要进入executeForIPage的那个方法,只需要将sql的返回值改成IPage对象或者实现类就行了

4、解决问题

sql的返回对象改Page对象,再次请求,发现recodes被自动赋值进去了

1
Page<User> queryList(@Param("page") Page<User> page);

image-20240223121522916

这样问题解决

以前之所以将sql的返回值设置为List,是为了方便其他查询的复用,那时只需要将page传入null即可,没想到引发了这样的问题。

其实这个bug还有其他的解决方法,就是在查询到数据list后,返回page对象前手动page.setRecords(list)

5、其他拓展

其实之前还好奇过mybatis-plustotal等参数,给sql的分页等是在哪里设置的,这次就一起看看在哪吧。

和上面的语句一样,把sql的返回值改成page对象,进入到MybatisMapperMethod#executeForIPage,断点打到sqlSession.selectList(command.getName(), param)方法上

image-20240226090748500

我们点进sqlSession.selectList方法,发现是一个接口,直接在接口方法打断点,运行时会带我们到对应的实现类上

image-20240226092533876

image-20240226092646937

同样的方法点进this.sqlSessionProxy.selectList(statement, parameter),运行几次后,就到了DefaultSqlSession#selectList方法

然后在executor.query(ms, wrapCollection(parameter), rowBounds, handler)这行打断点,点击进入方法内部

image-20240226092845358

然后就到了Plugin#invoke,可以看到,interceptor就是mybatis-plus的拦截器

image-20240226093300280

打断点进入后,这里就是进行page设置total等,还有分页的地方了

image-20240226093432332

具体的total查询,就在query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)

image-20240226093802346

可以看到,如果查询结果的false的话,就直接返回空的集合,连原先的查询语句都不执行了。进入willDoQuery,可以看到查询total的语句

image-20240226094117581

回到MybatisPlusInterceptor#intercept,进行分页的地方就在查询total的下面那行代码

image-20240226094312830

该方法会进入PaginationInnerInterceptor#beforeQuery方法,调试下去会发现有使用page.offset()page.getSize()的地方,就是在这里进行分页

image-20240226094620850

总结下来,其实就是mybatis-plus实现了mybatisInterceptor拦截器,在里面进行查询total和将sql进行分页