一、介绍
- 实际工作中,当我们对数据进行列表查询时,往往需要进行2部分工作:1. 查询所需列表页数所对应的数据;2. 统计符合条件的数据总数。这必然造成至少需要编写写2个sql,无形中会增加我们的工作量。此外,当发生业务变动时,我们又需要同时改动2个sql。否则,必然会引起数据结果的不一致。
- 因此,我们需要一个简单易用的分页工具来辅助我们完成这个工作。
- PageHelper就是一款好用的开源免费的Mybatis第三方物理分页插件。
二、MyBatis
2.1 Mybatis框架
首先需要要在配置文件中配置一些东西,然后根据这些配置去创建一个会话工厂,再根据会话工厂创建会话,会话发出操作数据库的sql语句,然后通过执行器操作数据,再使用mappedStatement对数据进行封装。
2.2 Mybatis插件
Mybatis的插件,主要作用在Executor执行器与mappedeStatement之间,即mybatis可以在插件中获得要执行的sql语句。然后,在sql语句中添加limit语句,再对sql进行封装,从而可以实现分页处理。MyBatis插件通过拦截器实现,拦截执行Sql的四大对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler。需要注意的是,并不是这四大对象中的所有方法都能被拦截:
通过@Intercepts声明当前类是一个拦截器,后面的@Signature是标识需要拦截的方法签名,通过以下三个参数来确定
type:被拦截的类名
method:被拦截的方法名
args:标注方法的参数类型
Mybatis插件执行流程:
三、PageHelper初识
3.1 pageHelper依赖引入
- springboot:直接引入pagehelper-spring-boot-starter
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
- springmvc:引入 pageHelper
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.1</version>
</dependency>
3.2 pagehelper插件配置
- springboot:mybatis、db相关配置
reasonable属性:分页参数合理化,默认false禁用。
启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页
禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据。
supportMethodsArguments属性:是否自动分页
params: count=countSql属性:如果POJO或者Map中发现了countSql属性,就作为count参数使用
- springmvc:配置mybatis-config.xml,并在配置数据源的时候,将mybatis配置文件指向以上文件。
-
3.3 Pagehelper使用
使用的时候,只需在查询list前,调用 startPage 设置分页信息,即可使用分页功能。如果不想进行count, 只要查分页数据, 则调用: PageHelper.startPage(pageNum, pageSize, false); 即可, 避免了不必要的count消耗。
/**
* 开始分页
*
* @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;
}
四、 pageHelper实现原理
4.1 springboot中接入interceptor
springboot 中接入pagehelper非常简单, 益于PageHelperAutoConfiguration初始化的方式, 它会自动加载配置。
@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties(PageHelperProperties.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
@Lazy(false)
public class PageHelperAutoConfiguration {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@Autowired
private PageHelperProperties properties;
/**
* 接受分页插件额外的属性
*
* @return
*/
@Bean
@ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
public Properties pageHelperProperties() {
return new Properties();
}
@PostConstruct
public void addPageInterceptor() {
PageInterceptor interceptor = new PageInterceptor();
Properties properties = new Properties();
//先把一般方式配置的属性放进去
properties.putAll(pageHelperProperties());
//在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是 closeConn,所以需要额外的一步
properties.putAll(this.properties.getProperties());
interceptor.setProperties(properties);
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
if (!containsInterceptor(configuration, interceptor)) {
configuration.addInterceptor(interceptor);
}
}
}
借助springboot的自动配置, 获取mybatis的sqlSessionFactoryList, 依次将PageInterceptor,也就是PageHelper接入其中。
@Override
public void setProperties(Properties properties) {
//缓存 count ms
msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
String dialectClass = properties.getProperty("dialect");
if (StringUtil.isEmpty(dialectClass)) {
dialectClass = default_dialect_class;
}
try {
Class<?> aClass = Class.forName(dialectClass);
dialect = (Dialect) aClass.newInstance();
} catch (Exception e) {
throw new PageException(e);
}
dialect.setProperties(properties);
String countSuffix = properties.getProperty("countSuffix");
if (StringUtil.isNotEmpty(countSuffix)) {
this.countSuffix = countSuffix;
}
}
4.2 interceptor的初始化
将 interceptor 添加到mybatis上下文后, 会在每次调用查询时进行拦截请求, 它的初始化也会在这时候触发,
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
4.3 PageInterceptor的调用过程
在executor被代理后, 会继续执行查询动作, 这时就会被PageInterceptor拦截。PageInterceptor的大体执行框架:
1. 先解析各位置参数;
2. 初始化 pageHelper 实例, 即 dialect;
3. 调用方法判断是否需要进行分页,如果不需要,直接返回结果;
4. 判断是否要进行count, 如果需要则实现一次count, ;
5. 查询分页结果;
6. 封装带分页的结果返回;
这个方法的逻辑比较多,我们主要是关注ExecutorUtil.pageQuery这个调用,因为PageHelper在这里面改写了sql语句:
@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) {
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
checkDialectExists();
//对 boundSql 的拦截处理
if (dialect instanceof BoundSqlInterceptor.Chain) {
boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
}
List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查询总数
Long count = count(executor, ms, parameter, rowBounds, null, 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();
}
}
}
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)) {
//生成分页的缓存 key
CacheKey pageKey = cacheKey;
//处理参数对象
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
//调用方言获取分页 sql
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
//设置动态参数
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//对 boundSql 的拦截处理
if (dialect instanceof BoundSqlInterceptor.Chain) {
pageBoundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.PAGE_SQL, pageBoundSql, pageKey);
}
//执行分页查询
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不执行分页的情况下,也不执行内存分页
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
}
/**
* @author liuzh
*/
public class MySqlDialect extends AbstractHelperDialect {
@Override
public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());
paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());
//处理pageKey
pageKey.update(page.getStartRow());
pageKey.update(page.getPageSize());
//处理参数配置
if (boundSql.getParameterMappings() != null) {
List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>(boundSql.getParameterMappings());
if (page.getStartRow() == 0) {
newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, int.class).build());
} else {
newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_FIRST, long.class).build());
newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, int.class).build());
}
MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
metaObject.setValue("parameterMappings", newParameterMappings);
}
return paramMap;
}
@Override
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("\n LIMIT ? ");
} else {
sqlBuilder.append("\n LIMIT ?, ? ");
}
return sqlBuilder.toString();
}
经过上面的sql重组之后,就可以得到具体分页的list数据了, 返回的也是list数据. 那么, 用户如何获取其他的分页信息呢? 比如count值去了哪里? 实际上, 在list 返回之后, 还有一个 afterPage 的动作要做, 而它的作用就是封装list 为带page信息的list。
@Override
public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
Page page = getLocalPage();
if (page == null) {
return pageList;
}
page.addAll(pageList);
if (!page.isCount()) {
page.setTotal(-1);
} else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
page.setTotal(pageList.size());
} else if (page.isOrderByOnly()) {
page.setTotal(pageList.size());
}
return page;
}
4.4 PageHelper只对startPage后的第一条select语句有效
@Override
public void afterAll() {
//这个方法即使不分页也会被执行,所以要判断 null
AbstractHelperDialect delegate = autoDialect.getDelegate();
if (delegate != null) {
delegate.afterAll();
autoDialect.clearDelegate();
}
clearPage();
}
在finally内把ThreadLocal中的分页数据给清除掉了,所以只要执行一次查询语句就会清除分页信息,故而后面的select语句自然就无效了。
4.5 PageHelper分页失效问题
通过前面的分析,我们了解到PageHelper经过sql重组之后,可以得到具体分页的list数据:
@Override
public List<TestVO> select(TestParam param) {
return this.testMapper.queryList(param);
}
这里可能会有一些疑问,如上面的列子, 我们执行sql通常返回的是List类数据,而执行拦截器PageHelper返回的是Page类的数据,这两个类是如何转换的?实际上,Page类是List的子类:
/**
* Mybatis - 分页对象
*
* @author liuzh/abel533/isea533
* @version 3.6.0
* 项目地址 : http://git.oschina.net/free/Mybatis_PageHelper
*/
public class Page<E> extends ArrayList<E> implements Closeable {
虽然,我们代码里sql查询声明的数据是List类,但实际上返回的是含有分页信息的Page类数据。所以,当对查询出来的数据进行一些处理后,再从controller里使用pagehelper时取出来的数据可能会有问题,比如total 始终等于pageSize。此时,需要检查处理封装后的数据list 是否为带page信息的list,如为否,需要对列表数据做一次转换:
@Override
public List<TestVo> select(TestParam param) {
List<TestConfig> configList = this.testMapper.testList(param);
PageInfo<Test> configListPageInfo = new PageInfo<>(configList);
Page<TestVo> page = new Page<>(configListPageInfo.getPageNum(), configListPageInfo.getPageSize());
page.setTotal(configListPageInfo.getTotal());
for (TestConfig testConfig : configList) {
TestConfigVo testConfigVo = new TestConfigVo();
BeanUtils.copyProperties(testConfig , testConfigVo);
page.add(testConfigVo );
}
return page;
}