在使用springboot开发系统时,列表查询经常会用PageHelper来进行分页。使用起来很方便,但从未想过它的实现原理,所以对其进行解读。
@Service
public class ScUserServiceImpl extends ServiceImpl<ScUserMapper, ScUser> implements IScUserService {
@Override
public PageInfo<ScUser> pageList(UserListF form) {
LambdaQueryWrapper<ScUser> qw = Wrappers.<ScUser>lambdaQuery()
.like(ScUser::getName, form.getName());
// 开启分页,对下一条执行的sql进行分页
Page<ScUser> page = PageHelper.startPage(form.getPageNum(), form.getPageSize());
List<ScUser> userList = baseMapper.selectList(qw);
return new PageInfo<>(page);
}
}
基于PageHelper(v5.3.2)官方文档,这是一个最简单、最常用的分页demo,持久层框架使用的是Mybatis-plus.
调试截图:
调试过程发现3个问题点:
- 为什么调用PageHelper.startPage()方法后可以实现分页?
- 为什么在执行sql查询前先执行select count(0) ?
- 为什么查询出来的userList不需要return,直接return new PageInfo<>(page)就可以有数据?
追踪过程:
进入PageHelper.startPage()方法,看到里面有一个 setLocalPage(page) 方法,方法的入参page里包含了分页参数pageNum、pageSize等
setLocalPage方法对 LOCAL_PAGE 这个线程局部变量进行进行赋值
在getLocaPage方法打断点,看什么时候会取出LOCAL_PAGE线程局部变量。最终发现方法落在了com.github.pagehelper.PageHelper#doBoundSql
同时在该方法的第二个入参boundSql里可以下一条要执行的sql
继续追踪,看到它的上级调用方法在com.github.pagehelper.PageInterceptor#intercept
PageInterceptor这个类实现了ibatis的Interceptor接口,从而实现了对sql语句的拦截
intercept()这个方法的中文注释非常完整,基本可以对照着阅读源码。
其中 dialect.beforeCount()这个方法会去线程局部变量LOCAL_PAGE中拿到count属性,从而决定是否在分页查询前执行count(0)查询。而这个count属性在一开始我们调用PageHelper.startPage()的时候,就配置了默认值true。
而 ExecutorUtil.pageQuery() 则是对查询sql进行解析(配置了多种解析器兼容mysql、oracle、sqlserver、Oscar等),并组装成对应的分页sql。组装好的sql将其封装为BoundSql对象,再调用org.apache.ibatis.executor.Executor#query()方法执行,最终拿到分页查询结果。
根据现有的结论我们可以得出,分页参数是通过ThreadLocal<Page>的方式传递的,那么第3个问题,为什么分页查询结果集不需要return而是直接return page对象,答案也就水落石出了。
在分页查询结束后,dialect.afterPage()方法会将结果集resultList塞入到LOCAL_PAGE 这个线程局部变量里去。
问题点解答:
1、为什么调用PageHelper.startPage()方法后可以实现分页?
因为startPage方法将分页参数保存到了线程局部变量,然后通过PageInterceptor(它实现了ibatis提供的inteceptor接口)对下一条即将执行的query sql进行拦截,解析语义并组装为分页query sql,执行并拿到结果集,并存放到线程局部变量里去。
2、为什么在执行sql查询前先执行select count(0) ?
因为调用 PageHelper.startPage(form.getPageNum(), form.getPageSize()) 方法时传递了默认count属性值,默认为true,所以在分页时会执行count(0)查询总记录数。它并不影响分页的过程、结果,它只是满足业务需要所做的一个查询。
如果不需要count(0),可以调用 PageHelper.startPage(form.getPageNum(), form.getPageSize(), false) 来指定。
3、为什么查询出来的userList不需要return,直接return new PageInfo<>(page)就可以有数据?
因为page是一个线程局部变量,分页查询结束后,PageInterceptor会调用 dialect.afterPage() 将结果集保存到page里去,所以page里会有数据。
你也可以return userList,但是page里的属性更加丰富,它除了结果集之外,还有当前页、每页记录数、总记录数等等.....
小结:
经过代码追踪,我明白了PageHelper的本质其实是基于ThreadLocal和ibatis提供的sql监听器实现的,解开了这层神秘的面纱。这种对sql约定好的的统一处理策略,非常值得学习。对也让我对mybatis的的执行过程更加好奇,后续可以更进一步对mybatis的原理进行追踪和理解。