1、引入依赖
只引入 PageHelper 不会自动适配 SpringBoot 失效,还需要整合依赖 pagehelper-spring-boot-autoconfigure
1 2 3 4 5 6 7 8 9 10 11 12
| <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency>
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.2.0</version> </dependency>
|
2、配置文件配置相应的参数
application.yml
1 2 3 4 5 6
| pagehelper: auto-dialect: on reasonable: true support-methods-arguments: true page-size-zero: true params: count=countSql
|
application.properties
1 2 3 4 5 6 7 8 9 10
| pagehelper.helper-dialect=on
pagehelper.reasonable=true
pagehelper.support-methods-arguments=true
pagehelper.pageSizeZero=true
pagehelper.params=count=countSql
|
3、使用
1 2 3 4 5 6 7 8 9 10 11
|
@ApiOperation(value = "查看后台文章") @GetMapping("/admin/articles") public ApiResponse<PageResult<ArticleBackDTO>> listArticleBacks(ConditionVO conditionVO) { return ApiResponse.ok(articleService.listArticleBacks(conditionVO)); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Override public PageResult<ArticleBackDTO> listArticleBacks(ConditionVO condition) { Integer count = articleMapper.countArticleBacks(condition); if (count == 0) { return new PageResult<>(); } PageHelper.startPage(condition.getCurrent(), condition.getSize()); List<ArticleBackDTO> articleBackDTOList = articleMapper.listArticleBacks(condition); Map<Object, Double> viewsCountMap = redisService.zAllScore(ARTICLE_VIEWS_COUNT); Map<String, Object> likeCountMap = redisService.hGetAll(ARTICLE_LIKE_COUNT); articleBackDTOList.forEach(item -> { Double viewsCount = viewsCountMap.get(item.getId()); if (Objects.nonNull(viewsCount)) { item.setViewsCount(viewsCount.intValue()); } item.setLikeCount((Integer) likeCountMap.get(item.getId().toString())); }); return new PageResult<>(articleBackDTOList, count);
}
|
4、使用提示
- 只有紧跟在
PageHelper.startPage()
方法后的第一个 Mybatis 的查询(Select)方法会被分页。
- 请不要在系统中配置多个分页插件(使用 Spring 时,mybatis-config.xml 和 Spring 配置方式,请选择其中一种,不要同时配置多个分页插件)!
- 对于带有 for update 的 sql,会抛出运行时异常,对于这样的 sql 建议手动分页,毕竟这样的 sql 需要重视。
- 由于嵌套结果方式会导致结果集被折叠,因此分页查询的结果在折叠后总数会减少,所以无法保证分页结果数量正确。
5. 源码分析
我们先来看看 startPage 方法。进入此方法,发现一堆方法重载,最后进入真正的 startPage
方法,有 5 个参数,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
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); Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } setLocalPage(page); return page; }
|
getLocalPage
和setLocalPage
方法做了什么操作?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
public abstract class PageMethod { protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>(); protected static boolean DEFAULT_COUNT = true;
protected static void setLocalPage(Page page) { LOCAL_PAGE.set(page); }
public static <T> Page<T> getLocalPage() { return LOCAL_PAGE.get(); }
public static void clearPage() { LOCAL_PAGE.remove(); }
public static long count(ISelect select) { Page<?> page = startPage(1, -1, true); select.doSelect(); return page.getTotal(); }
|
原来是将 page 放入了 ThreadLocal<Page>
中。ThreadLocal
是每个线程独有的变量,与其他线程不影响,是放置 page 的好地方。setLocalPage
之后,一定有地方 getLocalPage
Spring Boot 为我们做了什么
我们找到 PageHelper 的自动配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Configuration @ConditionalOnBean(SqlSessionFactory.class) @EnableConfigurationProperties(PageHelperProperties.class) @AutoConfigureAfter(MybatisAutoConfiguration.class) public class PageHelperAutoConfiguration {
@Autowired private List<SqlSessionFactory> sqlSessionFactoryList;
@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); } }
}
|
我们会发现,Spring Boot 在启动的时候会调用 addPageInterceptor()
方法,为所有的 SqlSessionFactory
添加 PageHelper 的拦截器。
分页插件的使用,首先是在 Mybatis 里面配置了分页拦截器(PageInterceptor),即在执行相关 SQL 之前会拦截做一点事情,所以应该就是在执行selectList
的时候,会自动为 SQL 加上limit 1,8
。
PageHelper 实际拦截 SQL
Mybatis 拦截器可以对下面 4 种对象进行拦截:
拦截对象 |
拦截方法 |
方法作用 |
Executor |
update |
对应 insert,delete,update 语句 |
|
query |
对应 select 语句 |
|
flushStatements |
刷新 Statement |
|
commit |
提交事务 |
|
rollback |
回滚事务 |
|
getTransaction |
获取事务 |
|
close |
关闭事务 |
|
isClosed |
判断事务是否关闭 |
StatementHandler |
prepare |
预编译 SQL |
|
parameterize |
设置参数 |
|
batch |
批处理 |
|
update |
对应 insert,delete,update 语句 |
|
query |
对应 select 语句 |
ParameterHandler |
getParameterObject |
获取参数 |
|
setParameters |
设置参数 |
ResultSetHandler |
handleResultSets |
处理结果集 |
|
handleOutputParameters |
处理存储过程出参 |
- Executor:mybatis 的内部执行器,作为调度核心负责调用 StatementHandler 操作数据库,并把结果集通过 ResultSetHandler 进行自动映射
- StatementHandler: 封装了 JDBC Statement 操作,是 sql 语法的构建器,负责和数据库进行交互执行 sql 语句
- ParameterHandler:作为处理 sql 参数设置的对象,主要实现读取参数和对 PreparedStatement 的参数进行赋值
- ResultSetHandler:处理 Statement 执行完成后返回结果集的接口对象,mybatis 通过它把 ResultSet 集合映射成实体对象
而 PageHelper 就是拦截 Excutor 的 query 方法,来看一下在 stater 中自动加载的 PageInterceptor interceptor = new PageInterceptor();分页拦截器源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| @Intercepts( { @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), } ) public class PageInterceptor implements Interceptor { private volatile Dialect dialect; private String countSuffix = "_COUNT"; protected Cache<String, MappedStatement> msCountMap = null; private String default_dialect_class = "com.github.pagehelper.PageHelper";
@Override public Object intercept(Invocation invocation) throws Throwable { try { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameter = args[1]; RowBounds rowBounds = (RowBounds) args[2]; ResultHandler resultHandler = (ResultHandler) args[3]; Executor executor = (Executor) invocation.getTarget(); CacheKey cacheKey; BoundSql boundSql; if (args.length == 4) { boundSql = ms.getBoundSql(parameter); cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql); } else { cacheKey = (CacheKey) args[4]; boundSql = (BoundSql) args[5]; } checkDialectExists(); if (dialect instanceof BoundSqlInterceptor.Chain) { boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey); } List resultList; if (!dialect.skip(ms, parameter, rowBounds)) { if (dialect.beforeCount(ms, parameter, rowBounds)) { Long count = count(executor, ms, parameter, rowBounds, null, boundSql); if (!dialect.afterCount(count, parameter, rowBounds)) { return dialect.afterPage(new ArrayList(), parameter, rowBounds); } } resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey); } else { resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } return dialect.afterPage(resultList, parameter, rowBounds); } finally { if(dialect != null){ dialect.afterAll(); } } }
|
其中resultList = ExecutorUtil.pageQuery(。。。);
是关键代码,继续看
转到 ExecutorUtil 抽象类的pageQuery
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey) throws SQLException { if(!dialect.beforePage(ms, parameter, rowBounds)) { return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql); } else { parameter = dialect.processParameterObject(ms, parameter, boundSql, cacheKey); String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey); BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter); Map<String, Object> additionalParameters = getAdditionalParameter(boundSql); Iterator var12 = additionalParameters.keySet().iterator();
while(var12.hasNext()) { String key = (String)var12.next(); pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); }
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql); } }
|
在抽象类 AbstractHelperDialect 的getPageSql
获取到对应的 Page 对象
1 2 3 4 5 6 7 8 9 10
| public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) { String sql = boundSql.getSql(); Page page = this.getLocalPage(); String orderBy = page.getOrderBy(); if(StringUtil.isNotEmpty(orderBy)) { pageKey.update(orderBy); sql = OrderByParser.converToOrderBySql(sql, orderBy); } return page.isOrderByOnly()?sql:this.getPageSql(sql, page, pageKey); }
|
进入到 MySqlDialect 类的getPageSql
方法进行 SQL 封装,根据 page 对象信息增加 Limit。分页的信息就是这么拼装起来的
1 2 3 4 5 6 7 8 9 10
| 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(); }
|
将最后拼装好的 SQL 返回给 DefaultSqlSession 执行查询并返回
1 2 3 4 5 6 7 8 9 10
| public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
|
至此整个查询过程完成,原来 PageHelper 的分页功能是通过 Limit 拼接 SQL 实现的。
PageHelper 的分页原理,最核心的部分是实现了 MyBatis 的 Interceptor 接口,从而将分页参数拦截在执行 sql 之前,拼装出分页 sql 到数据库中执行。
初始化自动配置的时候,将 PageInterceptor 初始化并加入 MyBatis 拦截器,记录在 interceptorChain 中。
执行的时候,PageHelper 首先将 page 需求记录在 ThreadLocal<Page>
中,然后在拦截的时候,从 ThreadLocal<Page>
中取出 page,拼装出分页 sql,然后执行。
同时将结果分页信息(包括当前页,每页条数,总页数,总记录数等)设置回 page,让业务代码可以获取。
6. 总结
PageHelper 作为 GitHub 上现在近 10K 的开源分页框架,也许代码深度和广度不及主流市场框架和技术,虽然在功能的实现和原理上,造轮子的难度不高,源码也很清晰,但是在很大程度上解决了很多基于 MyBatis 的分页技术难题,简化并提示了广大开发者的效率,这才是开发者们在开发的路上应该向往并为之拼搏的方向和道路. 作为受益者,也不应当仅仅是对其进行基本的使用,开发之余,也应该关注一些框架的拓展,对框架的底层有一定程度上的了解,并为之拓展和优化