PageHelper是MyBatis中一个很好用的分页插件,在集成插件之后,只需要在需要分页的MyBatis查询方法前调用PageHelper.startPage静态方法,紧跟在这个方法后的第一个MyBatis查询方法会被进行分页。
//增加此行代码开启分页,pageNum为第几页,pageSize为一页多少条
Page<ArticleVO> page = PageHelper.startPage(pageNum, pageSize);
//执行正常的sql查询
articleMapper.selectAll(query);
简单好用,但是实现原理是什么呢?
总结实现原理:
1、插件是通过MyBatis的拦截器来实现分页逻辑的,入口是一个PageInterceptor类。
2、通过ThreadLocal线程变量的形式增加需要分页的标识以及分页信息,这也就是PageHelper.startPage能开启实现分页的原因。
3、在MySql下(其他数据库没去研究),最终实现分页的查询操作,还是通过给sql添加limit x,x实现的。
准备工作:
使用的源码版本为:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.10</version>
</dependency>
由于项目中使用的是SpringBoot,所以引入的是starter的依赖就够了,使用的是:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.12</version>
</dependency>
分页信息及分页标识
聚焦于PageHelper.startPage这个静态方法,startPage有很多重载方法,但是最后调用的是:
/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
* @param count 是否进行count查询
* @param reasonable 分页合理化,null时用默认配置
* @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
*/
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}
方法的逻辑是:实例化一个Page对象,这个对象用来存放分页相关的信息,页码,每页显示数量等。然后通过setLocalPage方法将这个Page对象存储到线程变量ThreadLocal<Page> LOCAL_PAGE
中。到这里分页信息的设置就结束了,剩下的工作就是拦截器里面的内容,拦截器会通过LOCAL_PAGE中这个Page对象的存在与否决定是否开启分页逻辑。
分页的核心逻辑PageInterceptor拦截器
由于使用的是starter的依赖,所以在代码中是通过PageHelperAutoConfiguration这个配置类将PageInterceptor拦截器添加到MyBatis中的
@PostConstruct
public void addPageInterceptor() {
PageInterceptor interceptor = new PageInterceptor();
Properties properties = new Properties();
properties.putAll(pageHelperProperties());
properties.putAll(this.properties.getProperties());
interceptor.setProperties(properties);
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
//添加分页拦截器
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
通过@PostConstruct注解执行addInterceptor方法来添加拦截器。
整个分页的核心代码逻辑就在于拦截器中,拦截器是分页的入口
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
// 省略部分源码
List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查询总数
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
//处理查询总数,返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
if(dialect != null){
dialect.afterAll();
}
}
}
分页的基本逻辑都在这个方法中,通过dialect对象的各个方法结合实现分页功能,Dialect是一个接口,他有不同的子类实现,对应不同的数据库方言。如果我们没有在配置中指定具体的实现类的话,默认的实现类是PageHelper。而PageHelper相当于一个大管家,他内部有一个PageAutoDialect自动方言类来自动的选择对应的数据库方言。
PageAutoDialect识别数据库的实现原理是通过获取DataSource对象继而获取jdbcUrl,然后通过jdbcUrl来识别的,也就是通过识别jdbc:mysql://
实现
/**
* 根据 jdbcUrl 获取数据库方言
*
* @param ms
* @return
*/
private AbstractHelperDialect getDialect(MappedStatement ms) {
//改为对dataSource做缓存
DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource();
String url = getUrl(dataSource);
if (urlDialectMap.containsKey(url)) {
return urlDialectMap.get(url);
}
try {
lock.lock();
if (urlDialectMap.containsKey(url)) {
return urlDialectMap.get(url);
}
if (StringUtil.isEmpty(url)) {
throw new PageException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
}
String dialectStr = fromJdbcUrl(url);
if (dialectStr == null) {
throw new PageException("无法自动获取数据库类型,请通过 helperDialect 参数指定!");
}
AbstractHelperDialect dialect = initDialect(dialectStr, properties);
urlDialectMap.put(url, dialect);
return dialect;
} finally {
lock.unlock();
}
}
回到intercept方法看分页的主流程,流程主要是:是否需要分页(skip)->分页前是否要count查询(beforeCount)->count查询(count)->查询分页数据(ExecutorUtil.pageQuery)->处理分页结果(afterPage)->清理数据(afterAll)
skip方法:逻辑就是判断线程变量LOCAL_PAGE是否有page对象,如果没有的话看是否有原始的rowBounds对象
beforeCount方法:page对象中的count属性判断,默认是true
count方法:会通过主语句的id+_COUNT
后缀的形式查找是否有自定义的count查询语句,如果没有的话则会自动创建一个;自动创建的count语句是在com.github.pagehelper.parser.CountSqlParser#getSmartCountSql(java.lang.String, java.lang.String)
方法中,大部分是通过创建简单的count子查询实现
ExecutorUtil.pageQuery方法:这个方法比较复杂,是实现最终数据查询的地方,主要的逻辑是获取对应数据库方言的分页语句形式,MySql的话是在com.github.pagehelper.dialect.helper.MySqlDialect#getPageSql
方法中,通过添加limit实现
afterPage方法:返回结果的整理,将查询的数据结果以及分页的信息(总数总页数等)统一聚合设置到page对象中
afterAll方法:清理线程变量中的page对象,避免影响下一个sql的执行
到这里分页就完成了,更多的逻辑可以下载源码仔细看看,本地debug一下更高效。