欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

MyBatis的分页原理

时间:2023-08-09

写作目的

最近看到了一篇MyBatis的分页实现原理,文章里描述到使用ThreadLocal,其实想主要想看看ThreadLocal的巧妙使用,并且看一下分页是如何实现的。

源码下载

ChaiRongD/Demooo - Gitee.com

源码跟踪

其实一个简单的分页如下面代码所示,使用PageHelp对象设置分页的参数,然后把查询到的List对象作为参数传入PageInfo对象中,就拿到了分页对象的结果。

@GetMapping("/page") public Object page() { //查询第三页,每页三条 PageHelper.startPage(3 , 3); List temperatures = temperatureDao.selectByExample(null); //得到分页的结果对象 PageInfo resPage = new PageInfo<>(temperatures); return resPage; }

PageHelper.startPage方法

一直跟下去会定位到PageMethod的startPage方法,方法内容为创建一个包含分页参数的page对象,然后放在ThreadLocal中。

public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { //构建一个包含分页参数的page对象 //构建一个包含分页参数的page对象 //构建一个包含分页参数的page对象 Page page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); //当已经执行过orderBy的时候 Page oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByonly()) { page.setOrderBy(oldPage.getOrderBy()); } //把page对象放在ThreadLocal中 //把page对象放在ThreadLocal中 //把page对象放在ThreadLocal中 setLocalPage(page); return page; }

真正的执行逻辑

即执行dao.select方法

List 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###afterPagepublic 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 extends ArrayList )类型的,Page集成了ArrayList对象。

下面看PageInfo的构造方法,真是一看吓一跳。首先list参数传入的是Page对象,可以从Page对象中拿到total、pageNum、pageSize和当前页的数据集合,可以进一步算出是否为首页、尾页等其他非必要的分页信息。

public PageInfo(List 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)

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。