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与前端分页工具结合实现

掌握MyBatisPlus中的分页及条件查询构建 | 黑马程序员

手撕MybatisPlus分页原理

PageHelper和MybatisPlus的分页插件冲突

技术分享_MyBatisplus分页插件

MybatisPlus分页条件查询