pagerHelper与mybatisPlus分页冲突问题分析
Posted 醉酒的小男人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了pagerHelper与mybatisPlus分页冲突问题分析相关的知识,希望对你有一定的参考价值。
pagerHelper与mybatisPlus分页冲突问题分析
标题问题现象
在开发owner服务的时候发现,mybatisPlus分页不起作用,返回总数永远是0.
//mybatisPlus分页接口
public interface IService<T> {
//返回的page信息不包含total等信息,只显示一页内容
Page<T> selectPage(Page<T> page);
}
- jar包版本
- mybatis-plus 2.0.5
- pagehelper-spring-boot-starter 1.2.5
- pagehelper 5.1.4
问题分析
由于不管是pageHelper还是mybatis-plus分页的实现逻辑都是基于mybatis的拦截器来实现的,即Interceptor。因此初步推断是这里产生了冲突。
mybatis-plus实现逻辑
通过查看源码可以发现实现分页逻辑的拦截器是PaginationInterceptor
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class PaginationInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
if (target instanceof StatementHandler) {
StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget());
MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
RowBounds rowBounds = (RowBounds) metaStatementHandler.getValue("delegate.rowBounds");
//这里通过rowBounds的类型来判断是否分页
if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {
return invocation.proceed();
}
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
String originalSql = (String) boundSql.getSql();
if (rowBounds instanceof Pagination) {
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
Pagination page = (Pagination) rowBounds;
boolean orderBy = true;
if (page.isSearchCount()) {
CountOptimize countOptimize = SqlUtils.getCountOptimize(originalSql, optimizeType, dialectType,
page.isOptimizeCount());
orderBy = countOptimize.isOrderBy();
this.count(countOptimize.getCountSQL(), mappedStatement, boundSql, page);
if (page.getTotal() <= 0) {
return invocation.proceed();
}
}
String buildSql = SqlUtils.concatOrderBy(originalSql, page, orderBy);
originalSql = DialectFactory.buildPaginationSql(page, buildSql, dialectType, dialectClazz);
} else {
originalSql = DialectFactory.buildPaginationSql(rowBounds, originalSql, dialectType, dialectClazz);
}
metaStatementHandler.setValue("delegate.boundSql.sql", originalSql);
metaStatementHandler.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
}
return invocation.proceed();
}
}
通过上面的源码可以看出首先plus是通过拦截StatementHandler来实现逻辑的。
其次根据intercept方法可以大致猜测是否是因为某种原因导致RowBounds==null或者RowBounds是DEFAULT的,即没有offset和limit。
当然实际调试的结果确实如此,执行到这里的时候RowBounds是DEFAULT的。
pageHelper实现逻辑
@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 {
@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];
}
List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//反射获取动态参数
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//创建 count 查询的缓存 key
CacheKey countKey = executor.createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql);
countKey.update(MSUtils.COUNT);
MappedStatement countMs = msCountMap.get(countKey);
if (countMs == null) {
//根据当前的 ms 创建一个返回值为 Long 类型的 ms
countMs = MSUtils.newCountMappedStatement(ms);
msCountMap.put(countKey, countMs);
}
//调用方言获取 count sql
String countSql = dialect.getCountSql(ms, boundSql, parameter, rowBounds, countKey);
countKey.update(countSql);
BoundSql countBoundSql = new BoundSql(ms.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);
//处理查询总数
//返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
//判断是否需要进行分页查询
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);
//设置动态参数
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//执行分页查询
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不执行分页的情况下,也不执行内存分页
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
} else {
//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
dialect.afterAll();
}
}
}
通过源码发现pageHelper的实现逻辑是通过拦截Executor来实现的,因此可以知道在和mybatis-plus共存的情况下是先执行的pageHelper拦截。
public class PageHelper extends PageMethod implements Dialect {
@Override
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
if(ms.getId().endsWith(MSUtils.COUNT)){
throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
}
//关注这个pege代码
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;
}
}
}
public class PageParams {
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(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;
}
}
其实通过上面代码我们可以发现有出乎我们直觉的地方。就是在我们并未调用PageHelper.startPage的情况下,即使ThreadLocal中并没有Page信息,但是如果发现rowBounds包含分页信息,pageHelper也会自动进行分页。
因此!dialect.skip(ms, parameter, rowBounds)这个逻辑的返回值是true。需要执行分页逻辑。
继续往下看发现执行完分页逻辑之后执行的是executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql)所以此处已经抹去了分页的信息,已经拼接进了sql中。因此再到执行Statement拦截器的时候已经没有了分页信息。所以plus的分页不会执行。
解决办法
- 放弃plus继续使用原生的mybatis开发,只使用pageHelper分页。
- 混合使用plus和pageHelper,分页通过pageHelper来做,其他业务逻辑延续plus。
public abstract class AbstractHelperDialect extends AbstractDialect implements Constant {
@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;
}
}
pageHelper在执行完executor逻辑最后会执行dialect.afterPage(resultList, parameter, rowBounds)对结果进行封装。最后返回一个Page。
public class Page<E> extends ArrayList<E> implements Closeable {
}
同时mybatis-plus的所有list接口都是返回的List,因此可以直接使用。
PageHelper.startPage(pageNum, pageSize);
List<T> listInfo = super.selectList(entityWrapper);
if (listInfo instanceof Page) {
Page<T> pages = (Page) listInfo;
//后续可以直接通过pages获取所有的分页信息
MybatisPlus与前端分页工具结合实现