写作目的
最近看到了一篇MyBatis的分页实现原理,文章里描述到使用ThreadLocal,其实想主要想看看ThreadLocal的巧妙使用,并且看一下分页是如何实现的。
源码下载
源码跟踪
其实一个简单的分页如下面代码所示,使用PageHelp对象设置分页的参数,然后把查询到的List对象作为参数传入PageInfo对象中,就拿到了分页对象的结果。
@GetMapping("/page")
public Object page() {
//查询第三页,每页三条
PageHelper.startPage(3 , 3);
List<Temperature> temperatures = temperatureDao.selectByExample(null);
//得到分页的结果对象
PageInfo<Temperature> resPage = new PageInfo<>(temperatures);
return resPage;
}
PageHelper.startPage方法
一直跟下去会定位到PageMethod的startPage方法,方法内容为创建一个包含分页参数的page对象,然后放在ThreadLocal中。
/**
* 开始分页
*
* @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对象
//构建一个包含分页参数的page对象
//构建一个包含分页参数的page对象
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());
}
//把page对象放在ThreadLocal中
//把page对象放在ThreadLocal中
//把page对象放在ThreadLocal中
setLocalPage(page);
return page;
}
真正的执行逻辑
即执行dao.select方法
List<Temperature> temperatures = temperatureDao.selectByExample(null);
下一步直接跳到PageInterceptor的intercept方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
//省略内容,省略内容,省略内容
List resultList;
//步骤1:调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//步骤2:查询总条数
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
//处理查询总数,返回 true 时继续分页查询,false 时直接返回
//步骤3:保存总条数
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
//步骤4:执行分页查询
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
//步骤5:封装结果
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
if(dialect != null){
dialect.afterAll();
}
}
}
步骤1:判断是否分页
首先根据PageHelper的skip方法查看是否需要分页,判断条件是ThreadLocal中是否有page对象,因为PageHelper.startPage方法放入到ThreadLocal中放入page对象,因此此处会判断为分页
步骤2:查询总条数
方法会定位到PageInterceptor的count方法的的代码
count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
方法executeAutoCount方法如下,
1)首先根据查询语句拼接count语句(select * from table where a ---> select count("0") from table where a)
2)然后执行SQL
3)拿到count结果
步骤3:保存总条数
首先从ThreadLocal中获取page对象,然后把总条数count放在page对象中,然后根据总条数和分页条件判断是否有必要查询,比如一共10条记录,你每页10条,你查第2页,那么就没必要去查询,因此11-20条记录不存在。
public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {
//获取ThreadLocal中的page对象
Page page = getLocalPage();
//保存count对象
page.setTotal(count);
if (rowBounds instanceof PageRowBounds) {
((PageRowBounds) rowBounds).setTotal(count);
}
//pageSize < 0 的时候,不执行分页查询
//pageSize = 0 的时候,还需要执行后续查询,但是不会分页
if (page.getPageSize() < 0) {
return false;
}
//根据总数量和你分页条件去判断是否有必要去做查询
return count > ((page.getPageNum() - 1) * page.getPageSize());
}
步骤4:执行分页查询
首先获取分页的SQL(slect * from tablle -> select * from table limit ? ,?),然后执行获取到结果
怎么获取分页的SQL呢?简单到没朋友
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ? ");
} else {
sqlBuilder.append(" LIMIT ?, ? ");
}
return sqlBuilder.toString();
}
步骤5:封装结果
还是把查询到的结果放到TheadLocal中的page对象中,然后返回page对象,此时page对象带有查询对象集合、分页条数、第几页。
//AbstractHelperDialect###afterPage
public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
Page page = getLocalPage();
if (page == null) {
return pageList;
}
page.addAll(pageList);
//省略
return page;
}
构造PageInfo对象
首先要明确的是下面代码中的temperatures对象是Page(Page<E> extends ArrayList<E> )类型的,Page集成了ArrayList对象。
下面看PageInfo的构造方法,真是一看吓一跳。首先list参数传入的是Page对象,可以从Page对象中拿到total、pageNum、pageSize和当前页的数据集合,可以进一步算出是否为首页、尾页等其他非必要的分页信息。
public PageInfo(List<T> list, int navigatePages) {
//把list对象强转为page对象,然后获取total总条数对象
super(list);
if (list instanceof Page) {
Page page = (Page) list;
//获取当前第几页
this.pageNum = page.getPageNum();
//获取每页大小
this.pageSize = page.getPageSize();
this.pages = page.getPages();
this.size = page.size();
//由于结果是>startRow的,所以实际的需要+1
if (this.size == 0) {
this.startRow = 0;
this.endRow = 0;
} else {
this.startRow = page.getStartRow() + 1;
//计算实际的endRow(最后一页的时候特殊)
this.endRow = this.startRow - 1 + this.size;
}
} else if (list instanceof Collection) {
this.pageNum = 1;
this.pageSize = list.size();
this.pages = this.pageSize > 0 ? 1 : 0;
this.size = list.size();
this.startRow = 0;
this.endRow = list.size() > 0 ? list.size() - 1 : 0;
}
if (list instanceof Collection) {
this.navigatePages = navigatePages;
//计算导航页
calcNavigatepageNums();
//计算前后页,第一页,最后一页
calcPage();
//判断页面边界
judgePageBoudary();
}
}
总结
分页过程
首先会把分页参数封装成Page对象放到ThreadLocal中
然后根据SQL进行拼接转换(select * from table where a) -> (select count("0") from table where a)和(select * from table where a limit ?,?)
有了total总条数、pageNum当前第几页、pageSize每页大小和当前页的数据,就可以算出分页的其他非必要信息(是否为首页,是否为尾页,总页数)
ThreadLocal对象的使用
ThreadLocal的巧妙使用(big)