Mybatis分页插件: pageHelper的使用及其原理解析
在实际工作中,很进行列表查询的场景,我们往往都需要做两个步骤:1. 查询所需页数对应数据;2. 统计符合条件的数据总数;而这,又会导致我们必然至少要写2个sql进行操作。这无形中增加了我们的工作量,另外,当发生需要变动时,我们又需要同时改动这两个sql,否则必然导致结果的不一致。
因此,我们需要一个简单易用的分页工具来帮我们完成这个工作了,需求明确,至于如何实现则各有千秋。而我们要说的 pageHelper则是这其中实现比较好的一件的组件了,我们就一起来看看如何使用它进行提升工作效率吧!
1. pageHelper 的依赖引入
pom.xml 中引入pageHelper依赖:
1. 如果是springboot, 则可以直接引入 pagehelper-spring-boot-starter, 它会帮我们省去许多不必要的配置。
com.github.pagehelper
pagehelper-spring-boot-starter
1.2.12
2. 如果是普通的springmvc 类的项目,则引入 pageHelper 即可。
com.github.pagehelper
pagehelper
5.1.10
2. pagehelper插件配置
1. 如果是springboot,则直接配置几个配置项即可:
# mybatis 相关配置
mybatis:
#... 其他配置信息
configuration-properties:
offsetAsPageNum: true
rowBoundsWithCount: true
reasonable: true
mapper-locations: mybatis/mapper/*.xml
简单回顾看下db配置:
# db 配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123
url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&charactorEncoding=utf8&&serverTimezone=Asia/Shanghai
2. 普通springmvc项目配置:mybatis-config.xml
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
并在配置数据源的时候,将mybatis配置文件指向以上文件。
3. pagehelper 的使用
使用的时候,只需在查询list前,调用 startPage 设置分页信息,即可使用分页功能。
public Object getUsers(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
// 不带分页的查询
List
list = userMapper.selectAllWithPage(null); // 可以将结果转换为 Page , 然后获取 count 和其他结果值
com.github.pagehelper.Page listWithPage = (com.github.pagehelper.Page) list;
System.out.println("listCnt:" + listWithPage.getTotal());
return list;
}
即使用时, 只需提前声明要分页的信息, 得到的结果就是有分页信息的了. 如果不想进行count, 只要查分页数据, 则调用: PageHelper.startPage(pageNum, pageSize, false); 即可, 避免了不必要的count消耗.
4. pageHelper 实现原理1: interceptor
mybatis 有个插件机制,可以支持外部应用进行任意扩展。它在启动的时候会将 interceptor 添加到mybatis的上下文中。然后在进行查询时再触发实例化动作.
4.1 springboot 中接入interceptor
springboot 中接入pagehelper非常简单, 主要受益于初始化的方式, 它会自动加载配置.
// com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration#addPageInterceptor
@PostConstruct
public void addPageInterceptor() {
// 初始化 com.github.pagehelper.PageInterceptor
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) {
// 添加inteceptor到 mybatis 中
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
// org.apache.ibatis.session.Configuration#addInterceptor
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
// org.apache.ibatis.plugin.InterceptorChain#addInterceptor
public void addInterceptor(Interceptor interceptor) {
// 使用 ArrayList 保存intceptor
interceptors.add(interceptor);
}
借助springboot的自动配置, 获取mybatis的sqlSessionFactoryList, 依次将 pagehelper 接入其中。
4.2 interceptor的初始化
将 interceptor 添加到mybatis上下文后, 会在每次调用查询时进行拦截请求, 它的初始化也会在这时候触发.
// org.apache.ibatis.session.Configuration#newExecutor
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);
}
// 以interceptorChain包装 executor, 以便inteceptor发挥作用
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
// org.apache.ibatis.plugin.InterceptorChain#pluginAll
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 使用plugin一层层包装 target, 具体实现为使用代理包装 target
// 所以, interceptor 的使用顺序是按照添加的顺序来的, 并不能自行设置
target = interceptor.plugin(target);
}
return target;
}
// com.github.pagehelper.PageInterceptor#plugin
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// org.apache.ibatis.plugin.Plugin#wrap
public static Object wrap(Object target, Interceptor interceptor) {
// 获取注解中说明的方式列表 @Intercepts -> @Signature, 下面我们看 pageInterceptor的注解
Map
, Set > signatureMap = getSignatureMap(interceptor); Class type = target.getClass();
// 过滤需要进行代理的接口, 而非全部代理
Class[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 使用jdk方式生成动态代理
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
// 使用 Plugin 包装代理实现
new Plugin(target, interceptor, signatureMap));
}
return target;
}
// pageInterceptor的注解, 即定义要拦截的方法列表
@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}),
}
)
// 过滤代理的接口
private static Class[] getAllInterfaces(Class type, Map
, Set > signatureMap) { Set
> interfaces = new HashSet<>(); while (type != null) {
for (Class c : type.getInterfaces()) {
// 只有设置了的接口才会被添加
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class[interfaces.size()]);
}
这样, interceptor 就和executor绑定了, 后续的查询将会看到interceptor 的作用.
4.3 interceptor的调用过程
在executor被代理后, 会继续执行查询动作, 这时就会被interceptor拦截了.
// org.apache.ibatis.plugin.Plugin#invoke
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set
methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) {
// 匹配的方法会被拦截, 即 query 方法
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
// pageHelper 正式起作用的入口
// com.github.pagehelper.PageInterceptor#intercept
@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();
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();
}
}
}
以上就是 pageHelper 的大体执行框架了:
1. 先解析各位置参数;
2. 初始化 pageHelper 实例, 即 dialect;
3. 调用方法判断是否需要进行分页,如果不需要,直接返回结果;
4. 判断是否要进行count, 如果需要则实现一次count, ;
5. 查询分页结果;
6. 封装带分页的结果返回;
下面我们就每个细节依次看看实现吧.
4.4 是否跳过分页判定
首先会进行是否需要跳过分页逻辑,如果跳过, 则直接执行mybatis的核心逻辑继续查询. 而是否要跳过分页, 则是通过直接获取page分页参数来决定的,没有分页参数设置,则跳过, 否则执行分页查询. 这算是分页的一个入口判定呢。
/**
* 跳过 count 和 分页查询
*
* @param ms MappedStatement
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @return true 跳过,返回默认查询结果,false 执行分页查询
*/
// com.github.pagehelper.PageHelper#skip
@Override
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
if (ms.getId().endsWith(MSUtils.COUNT)) {
throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
}
// 如果 page 返回null, 则不需要进行分页, 即是否调用 PageHelper.start(pageNo, pageSize) 方法
Page page = pageParams.getPage(parameterObject, rowBounds);
if (page == null) {
return true;
} else {
//设置默认的 count 列
if (StringUtil.isEmpty(page.getCountColumn())) {
page.setCountColumn(pageParams.getCountColumn());
}
autoDialect.initDelegateDialect(ms);
return false;
}
}
// com.github.pagehelper.page.PageAutoDialect#initDelegateDialect
//多数据动态获取时,每次需要初始化
public void initDelegateDialect(MappedStatement ms) {
if (delegate == null) {
if (autoDialect) {
// 比如 MySqlDialect
this.delegate = getDialect(ms);
} else {
dialectThreadLocal.set(getDialect(ms));
}
}
}
/**
* 获取分页参数
*/
// com.github.pagehelper.page.PageParams#getPage
public Page getPage(Object parameterObject, RowBounds rowBounds) {
Page page = PageHelper.getLocalPage();
if (page == null) {
if (rowBounds != RowBounds.DEFAULT) {
if (offsetAsPageNum) {
page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount);
} else {
page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount);
//offsetAsPageNum=false的时候,由于PageNum问题,不能使用reasonable,这里会强制为false
page.setReasonable(false);
}
if(rowBounds instanceof PageRowBounds){
PageRowBounds pageRowBounds = (PageRowBounds)rowBounds;
page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount());
}
} else if(parameterObject instanceof IPage || supportMethodsArguments){
try {
page = PageObjectUtil.getPageFromObject(parameterObject, false);
} catch (Exception e) {
return null;
}
}
if(page == null){
return null;
}
PageHelper.setLocalPage(page);
}
//分页合理化
if (page.getReasonable() == null) {
page.setReasonable(reasonable);
}
//当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
if (page.getPageSizeZero() == null) {
page.setPageSizeZero(pageSizeZero);
}
return page;
}
才上判定决定了后续的分页效果,主要是利用 ThreadLocal 来保存分页信息,从而与用户代码产生关联。
4.5 pageHelper 的 count 操作
判断是否是否需要count, 这些判定都会以 PageHelper 作为门面类进行接入, 而特殊地方则由具体方言实现.
// com.github.pagehelper.PageHelper#beforeCount
@Override
public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
return autoDialect.getDelegate().beforeCount(ms, parameterObject, rowBounds);
}
// com.github.pagehelper.dialect.AbstractHelperDialect#beforeCount
@Override
public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
// 获取page参数信息, 该参数设置在 ThreadLocal 中
Page page = getLocalPage();
return !page.isOrderByOnly() && page.isCount();
}
// 如果需要进行count, 则需要自行组装count逻辑进行查询.
// com.github.pagehelper.PageInterceptor#count
private Long count(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql) throws SQLException {
// 在原有list 查询后添加 _COUNT 代表count查询id
String countMsId = ms.getId() + countSuffix;
Long count;
//先判断是否存在手写的 count 查询
MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
if (countMs != null) {
count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
} else {
countMs = msCountMap.get(countMsId);
//自动创建
if (countMs == null) {
//根据当前的 ms 创建一个返回值为 Long 类型的 ms
countMs = MSUtils.newCountMappedStatement(ms, countMsId);
msCountMap.put(countMsId, countMs);
}
count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
}
return count;
}
// 创建count ms
// com.github.pagehelper.util.MSUtils#newCountMappedStatement(org.apache.ibatis.mapping.MappedStatement, java.lang.String)
public static MappedStatement newCountMappedStatement(MappedStatement ms, String newMsId) {
// 直接基于原有 sql 构建新的 MappedStatement
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), newMsId, ms.getSqlSource(), ms.getSqlCommandType());
builder.resource(ms.getResource());
// 注意此处并未使用到用户设置的分页参数
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
StringBuilder keyProperties = new StringBuilder();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
}
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
//count查询返回值int
List
resultMaps = new ArrayList (); ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), Long.class, EMPTY_RESULTMAPPING).build();
resultMaps.add(resultMap);
builder.resultMaps(resultMaps);
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
}
/**
* 执行自动生成的 count 查询
*/
// com.github.pagehelper.util.ExecutorUtil#executeAutoCount
public static Long executeAutoCount(Dialect dialect, Executor executor, MappedStatement countMs,
Object parameter, BoundSql boundSql,
RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
Map
additionalParameters = getAdditionalParameter(boundSql); //创建 count 查询的缓存 key
CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
//调用方言获取 count sql
String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey);
//countKey.update(countSql);
BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
//当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
for (String key : additionalParameters.keySet()) {
countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//执行 count 查询
Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
Long count = (Long) ((List) countResultList).get(0);
return count;
}
// com.github.pagehelper.PageHelper#getCountSql
@Override
public String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
// 委托给各方言实现 sql 组装
return autoDialect.getDelegate().getCountSql(ms, boundSql, parameterObject, rowBounds, countKey);
}
// com.github.pagehelper.dialect.AbstractHelperDialect#getCountSql
@Override
public String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
Page