Mybatis PageHelper实现分页的使用和过程分析(源码)

目录

前言(啥是PageHelper)

如何使用?(简单实现demo)

一,在SpringBoot工程Pom文件中添加如下依赖

二,Controller层(仅仅展示功能,没有写Service层)

三,mapper层

四,执行结果

过程探究 (源码预警)

一,传统分页原理

二,流程分析

1,设置分页

2,PageInterceptor拦截器以及流程

3,获取总条数

4,获取分页数据

5,查询结果

三,注意事项

总结


前言(啥是PageHelper)

        最近实习中,分配了一些分页功能的接口需要用到PageHelper来做,因此,一边做一遍学习一下这个插件的原理。

        PageHelper是Mybatis的一个插件,PageHelper可以通过设置  PageHelper.startPage(pageNum,pageSize);来执行分页功能,非常的方便快捷,这条语句后紧跟的一条查询操作会进行分页。下面我们来分析一下pageHelper的使用。

//第一页,一页20条记录 
PageHelper.startPage(1, 20);

如何使用?(简单实现demo)

一,在SpringBoot工程Pom文件中添加如下依赖

            <!-- mybatis 分页插件 -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>1.2.10</version>
            </dependency>

二,Controller层(仅仅展示功能,没有写Service层)

@GetMapping(value = "/list")
    public BaseResponse list(@RequestParam Integer pageNum) {
        log.info("列表接口入参:{}", pageNum);

        //设置分页,页码为前端传递,固定页码20页
        PageHelper.startPage(pageNum, 5);
        //查询记录,将记录存入PageInfo
        List<InsMarketChannel> info = channelMapper.selectAll();
        PageInfo<InsMarketChannel> pageInfo = new PageInfo<>(info);
        if(result == null){
            return BaseResponse.fail("查询分页失败");
        }
        return BaseResponse.ok(pageInfo ,"查询分页成功");
    }

三,mapper层

<select id="selectAll" resultMap="BaseResultMap">
        select
        *
        from ins_market_channel
</select>

使用Postman进行测试,查询第一页

四,执行结果

{

    "code": 200,

    "msg": "查询分页成功",

    "data": {

        "total": 14,

        "list": [

            {

                "channelId": 198149,

                "channelName": "Marbury",

                "description": "你好",

                "channelStatus": null,

                "autoUnpacking": null,

                "sendNumber": null,

                "sendPeriod": null,

                "timeUnit": null

            },

            {

                "channelId": 198150,

                "channelName": "Marburyy",

                "description": "你好",

                "channelStatus": null,

                "autoUnpacking": null,

                "sendNumber": null,

                "sendPeriod": null,

                "timeUnit": null

            },

            {

                "channelId": 198151,

                "channelName": "Marburyy",

                "description": "你好",

                "channelStatus": null,

                "autoUnpacking": null,

                "sendNumber": 2,

                "sendPeriod": 2,

                "timeUnit": null

            },

            {

                "channelId": 198153,

                "channelName": "Marburyy",

                "description": "你好",

                "channelStatus": null,

                "autoUnpacking": null,

                "sendNumber": 3,

                "sendPeriod": 3,

                "timeUnit": null

            },

            {

                "channelId": 198154,

                "channelName": "Marburyy",

                "description": "你好",

                "channelStatus": null,

                "autoUnpacking": null,

                "sendNumber": null,

                "sendPeriod": null,

                "timeUnit": null

            }

        ],

        "pageNum": 0,

        "pageSize": 0,

        "size": 0,

        "startRow": 0,

        "endRow": 0,

        "pages": 0,

        "prePage": 0,

        "nextPage": 0,

        "isFirstPage": false,

        "isLastPage": false,

        "hasPreviousPage": false,

        "hasNextPage": false,

        "navigatePages": 0,

        "navigatepageNums": null,

        "navigateFirstPage": 0,

        "navigateLastPage": 0

    },

    "success": false

}

由此可见,我们不仅得到了第一页的5条数据(list中),也得到了总记录条数(total),其他参数均是PageInfo中的属性。

                                                 

         至此,我们可能会有些疑问:为啥执行了那句PageHelper.startPage(pageNum, 5)后,仅仅进行数据库查询操作,就可以进行分页处理,而且连总记录数都拿到了呢?接下来让我们一探究竟!

过程探究 (源码预警)

                                        

一,传统分页原理

        首先我们要了解传统分页是怎么实现的。下面举个例子(查询第2页,每页5条记录):

 select from ins_market_channel LIMIT 5, 5

通过Limit关键字,第一个参数为从第几条数据后开始,第二各参数为一共几条数据。第2页即为从第6条数据开始,一共截取5条,即获取第6到第10条数据。PageHelper的底层也是这样工作的。

二,流程分析

1,设置分页

PageHelper.startPage(pageNum, 5);

        调用这个方法后,再经过层层调用会将页码和页面大小封装成Page对象,存入ThreadLocal中,以便后续进行分页操作时获取。下面上代码。

    /**
     * 开始分页
     *
     * @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对象
        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());
        }
        //存入ThreadLocal
        setLocalPage(page);
        return page;
    }
 

2,PageInterceptor拦截器以及流程

        为什么进行普通数据库查询操作就会进行分页呢?因为PageHelper内部实现了一个PageInterceptor拦截器,在Spring项目启动时,会将这个拦截器加入到拦截器链中,当进行select操作时,会对该行为进行拦截,查询总条数并且重新拼接select语句(添加Limit关键字)再进行查询。后面会详细分析。下面上代码。

debug调试,从数据库查询语句开始不断step进入下一层 直到

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        //这一句,对sql进行拦截
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

 

step进入intercept方法,一路debug发现如果有参数则获取参数,查询列表总数(重要),查询分页数据(重要),最后调用afterPage方法进行分页后的处理。重要部分下面继续深入。

@Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            //如果controller层调用的selectAll方法传入了参数,将从下面获取
            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 个参数时
                //通过MappedStatement拿到要执行的sql语句
                boundSql = ms.getBoundSql(parameter);
                //拿到所有需要用的元素,将他们缓存起来,以备后续用到,下面附图1
                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 {
            dialect.afterAll();
        }
    }

 

                                                                           图-1

3,获取总条数

接下来我们进入Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);这一个方法,继续深入探索。上代码。

 private Long count(Executor executor, MappedStatement ms, Object parameter,
                       RowBounds rowBounds, ResultHandler resultHandler,
                       BoundSql boundSql) throws SQLException {
        String countMsId = ms.getId() + countSuffix;
        Long count;
        //先判断是否存在手写的 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;
    }

 

继续step入count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);方法

 /**
     * 执行自动生成的 count 查询
     *
     * @param dialect
     * @param executor
     * @param countMs
     * @param parameter
     * @param boundSql
     * @param rowBounds
     * @param resultHandler
     * @return
     * @throws SQLException
     */
    public static Long executeAutoCount(Dialect dialect, Executor executor, MappedStatement countMs,
                                        Object parameter, BoundSql boundSql,
                                        RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
        //创建 count 查询的缓存 key,即是上面提前缓存好的CacheKey,里面存有分页查询的sql
        CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
        //调用方言获取 count sql,获取总条数的sql,附在图2
        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 查询,重要!执行下面的query方法后,执行上面的select *....语句
        Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
        Long count = (Long) ((List) countResultList).get(0);
        return count;
    }

                

                                                                        图-2

 执行executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);方法后,查询记录总数。如果开启sql日志,可在控制台看到以下输出。

回到上面的intercept方法,获取count后,调用afterCount方法,将count记录保存至之前缓存在ThreadLocal中的Page对象中。代码如下。

@Override
    public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {
        //从ThreadLocal中拿到page对象
        Page page = getLocalPage();
        //将记录总数存入page对象
        page.setTotal(count);
        if (rowBounds instanceof PageRowBounds) {
            ((PageRowBounds) rowBounds).setTotal(count);
        }
        //pageSize < 0 的时候,不执行分页查询
        //pageSize = 0 的时候,还需要执行后续查询,但是不会分页
        if (page.getPageSize() < 0) {
            return false;
        }
        return count > 0;
    }

                                         

4,获取分页数据

 至此,记录总数我们已经拿到了,接下来就是分页记录们了。废话少说,继续回到上面的intercept方法,step进入ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);方法。代码如下。

/**
     * 分页查询
     *
     * @param dialect
     * @param executor
     * @param ms
     * @param parameter
     * @param rowBounds
     * @param resultHandler
     * @param boundSql
     * @param cacheKey
     * @param <E>
     * @return
     * @throws SQLException
     */
    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,里面存有分页查询的sql
            CacheKey pageKey = cacheKey;
            //处理参数对象,从ThreadLocal中获取page对象(包含分页信息,前面已提到)
            parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
            //调用方言,拼接Limit关键字,获取分页 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));
            }
            //执行分页查询
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
        } else {
            //不执行分页的情况下,也不执行内存分页
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
        }
    }

 

        step进入dialect.processParameterObject(ms, parameter, boundSql, pageKey);

发现关键性代码如下。

 @Override
    public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
        //处理参数,从ThreadLocal中获取page分页参数!!!!!!
        Page page = getLocalPage();
        //如果只是 order by 就不必处理参数
        if (page.isOrderByOnly()) {
            return parameterObject;
        }
        Map<String, Object> paramMap = null;
        if (parameterObject == null) {
            paramMap = new HashMap<String, Object>();
        } else if (parameterObject instanceof Map) {
            //解决不可变Map的情况
            paramMap = new HashMap<String, Object>();
            paramMap.putAll((Map) parameterObject);
        } else {
            paramMap = new HashMap<String, Object>();
            //动态sql时的判断条件不会出现在ParameterMapping中,但是必须有,所以这里需要收集所有的getter属性
            //TypeHandlerRegistry可以直接处理的会作为一个直接使用的对象进行处理
            boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
            MetaObject metaObject = MetaObjectUtil.forObject(parameterObject);
            //需要针对注解形式的MyProviderSqlSource保存原值
            if (!hasTypeHandler) {
                for (String name : metaObject.getGetterNames()) {
                    paramMap.put(name, metaObject.getValue(name));
                }
            }
            //下面这段方法,主要解决一个常见类型的参数时的问题
            if (boundSql.getParameterMappings() != null && boundSql.getParameterMappings().size() > 0) {
                for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
                    String name = parameterMapping.getProperty();
                    if (!name.equals(PAGEPARAMETER_FIRST)
                            && !name.equals(PAGEPARAMETER_SECOND)
                            && paramMap.get(name) == null) {
                        if (hasTypeHandler
                                || parameterMapping.getJavaType().equals(parameterObject.getClass())) {
                            paramMap.put(name, parameterObject);
                            break;
                        }
                    }
                }
            }
        }
        return processPageParameter(ms, paramMap, page, boundSql, pageKey);
    }

 

        至此,我们已经拿到了分页需要用到的参数(页码,每页条数),只需再修改sql语句,进行查询,即可得到分页数据。

        回到pageQuery方法继续step进入dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);方法,不断step进入后发现其中奥妙(代码如下):

@Override
    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        //拼接分页sql
        if (page.getStartRow() == 0) {
            //如果第一页开始Limit只需要一个参数
            sqlBuilder.append(" LIMIT ? ");
        } else {
            //如果不是第一页则需要两个参数(前面分析过)
            sqlBuilder.append(" LIMIT ?, ? ");
        }
        return sqlBuilder.toString();
    }

 

 回到pageQuery中的executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);方法进行分页查询(反射细节不再描述)。执行该语句之后发现控制台打印输出:

         库表结构已打码   正常应该打印select * from ins_market_channel LIMIT ?  ::: [ 5]

 我懒得改业务代码了而已 - -

        此时我们发现List中已经有了5条页面记录,如图3

        总条数以及其他各种页面参数也存在了List对象中,如图4

 

5,查询结果

                                                                         图-3

                        

                                                                          图-4

        此时将该集合对象返回给前端,大功告成喽!!!

                                         

三,注意事项

         在文章开头我已经提过了:这条语句后紧跟的一条查询操作会进行分页。也就是说,再执行这个分页操作后,如果还想再进行一次分页操作,需要重新执行       PageHelper.startPage(pageNum, pageSize);

这条语句才可以再次进行分页。通俗来说:

 设置分页是一次性的!!

        由于我们都有刨根问底的优良品质,所以我们要弄懂为啥子是这样的机制捏?

我们重新回到intercept方法,所有逻辑全部执行完之后,会调用afterAll方法,该方法会将ThreadLocal中的Page对象删除,详见如下代码:

@Override
    public void afterAll() {
        //这个方法即使不分页也会被执行,所以要判断 null
        AbstractHelperDialect delegate = autoDialect.getDelegate();
        if (delegate != null) {
            delegate.afterAll();
            autoDialect.clearDelegate();
        }
        //清楚分页信息
        clearPage();
    }

step进入clearPage()方法

     /**
     * 移除本地变量
     */
    public static void clearPage() {
        //删除ThreadLocal中的数据
        LOCAL_PAGE.remove();
    }

总结

        至此,我们已经拿捏PageHelper了,难道不是么?

                                                         

        我是芜湖马老师,一个普普通通的22届毕业本科生。

        如果觉得文章还不错,点赞关注不迷路。

        后续会持续更新Java学习之路上遇到的问题以及解决方案,争取让大家都掰开了,吃透了。

        有啥问题欢迎和我交流。

 

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芜湖马老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值