PageHelper


PageHelper是常用的分页插件,之前只停留在用的阶段,直到…

发生了一次生产事故

所以准备把他的源码研究一下,看下原因

介绍(What)

image-20200530110229737

从这里看到,PageHelper的使用是建立在Mybatis基础之上的,具体实现后面再说。 同时支持常见的各种数据库如 Mysql、Oracle、H2等,基本都能支持。

使用(How)

重要提示

PageHelper.startPage方法重要提示:

只有紧跟在PageHelper.startPage方法后的第一个Mybatis的**查询(Select)**方法会被分页。

请不要配置多个分页插件

请不要在系统中配置多个分页插件(使用Spring时,mybatis-config.xmlSpring<bean>配置方式,请选择其中一种,不要同时配置多个分页插件)!

分页插件不支持带有for update语句的分页

对于带有for update的sql,会抛出运行时异常,对于这样的sql建议手动分页,毕竟这样的sql需要重视。

分页插件不支持嵌套结果映射

由于嵌套结果方式会导致结果集折叠,因此分页查询的结果折叠后总数会减少,所以无法保证分页结果数量正确。

初步怀疑我的问题是这一步引起,因为我是联表查询


1. 引入Maven依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>最新版本</version>
</dependency>

2. 配置拦截器

分页插件的使用是建立在 Mybatis拦截器上的,执行sql前会进行参数拼接

在 MyBatis 配置 xml 中配置拦截器插件

<plugins>
    <!-- com.github.pagehelper为PageHelper类所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
        <property name="param1" value="value1"/>
	</plugin>
</plugins>

3. 使用

最常见的用法

//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
//紧跟着的第一个select方法会被分页
List<User> list = userMapper.selectIf(1);
PageInfo page = new PageInfo(list);

原理(Why)

今天就来对源码一探究竟

PageHelper

从这个类开始

PageHelper.startPage(1,10);

我们点进去看看

		/**
     * 开始分页
     *
     * @param pageNum  页码
     * @param pageSize 每页显示数量
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize) {
        return startPage(pageNum, pageSize, true);
    }
    
    //多了一个是否进行count查询的参数,由上面看,默认开启
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
        return startPage(pageNum, pageSize, count, null);
    }

		//多了个 reasonable : 分页合理化
		public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable) {
        return startPage(pageNum, pageSize, count, reasonable, null);
    }

		//终于到方法体了,多了个 pageSizeZero
		//这样介绍的  你可以配置 pageSizeZero 为 true, 配置后,当 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果。
		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类
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);                    //下面这俩参数默认没有
        //当已经执行过orderBy的时候
        Page<E> oldPage = SqlUtil.getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        SqlUtil.setLocalPage(page);
        return page;
    }

流程就是新建一个Page类,主要给SqlUtil设置 LocalPage(), 看下LocalPage()实现

public class SqlUtil implements Constant {
    private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
}

是一个线程私有的 ThreadLocal,这个方法到此结束。

那么,设置完这个,还不知道怎么执行分页的。我们再回到PageHelper

怎么生效的

回到PageHelper后,我们看到 pageHelper 实现了 mybatis的拦截器

public class PageHelper implements Interceptor {

}

//该接口如下
package org.apache.ibatis.plugin;

import java.util.Properties;
public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;

    Object plugin(Object var1);

    void setProperties(Properties var1);
}

那么可以断言,在Mybatis执行的时候,PageHelper通过拦截器做了些工作,我们来看 pageHelper中的intercept方法

/**
     * Mybatis拦截器方法
     *
     * @param invocation 拦截器入参
     * @return 返回执行结果
     * @throws Throwable 抛出异常
     */
    public Object intercept(Invocation invocation) throws Throwable {
        if (autoRuntimeDialect) {      //自动检测 数据库类型
            SqlUtil sqlUtil = getSqlUtil(invocation);
            return sqlUtil.processPage(invocation);
        } else {
            if (autoDialect) {
                initSqlUtil(invocation);
            }
            return sqlUtil.processPage(invocation);
        }
    }

注释非常清晰,核心是 sqlUtil.processPage 方法,继续看

/**
     * Mybatis拦截器方法,这一步嵌套为了在出现异常时也可以清空Threadlocal
     *
     * @param invocation 拦截器入参
     * @return 返回执行结果
     * @throws Throwable 抛出异常
     */
    public Object processPage(Invocation invocation) throws Throwable {
        try {
            Object result = _processPage(invocation);
            return result;
        } finally {
            clearLocalPage();
        }
    }

继续看 _processPage()

image-20200530120547485

图中1是指:

supportMethodsArguments:支持通过 Mapper 接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。

2是真正处理的方法,我们放他出来, 比较长,但是耐心看看。

/**
     * Mybatis拦截器方法
     *
     * @param invocation 拦截器入参
     * @return 返回执行结果
     * @throws Throwable 抛出异常
     */
    private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
        //保存RowBounds状态
        RowBounds rowBounds = (RowBounds) args[2];
        //获取原始的ms,   这里即  包名 + 方法名定位的sql语句
        MappedStatement ms = (MappedStatement) args[0];
        //判断并处理为PageSqlSource
        if (!isPageSqlSource(ms)) {
            processMappedStatement(ms);
        }
        //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
        ((PageSqlSource)ms.getSqlSource()).setParser(parser);
        try {
            //忽略RowBounds-否则会进行Mybatis自带的内存分页
            args[2] = RowBounds.DEFAULT;
            //如果只进行排序 或 pageSizeZero的判断
            if (isQueryOnly(page)) {
                return doQueryOnly(page, invocation);
            }

            //简单的通过total的值来判断是否进行count查询
            if (page.isCount()) {
                page.setCountSignal(Boolean.TRUE);
                //替换MS
                args[0] = msCountMap.get(ms.getId());
                //查询总数
                Object result = invocation.proceed();
                //还原ms
                args[0] = ms;
                //设置总数
                page.setTotal((Integer) ((List) result).get(0));
                if (page.getTotal() == 0) {
                    return page;
                }
            } else {
                page.setTotal(-1l);
            }
            //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
            if (page.getPageSize() > 0 &&
                    ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                            || rowBounds != RowBounds.DEFAULT)) {
                //将参数中的MappedStatement替换为新的qs
                page.setCountSignal(null);
                BoundSql boundSql = ms.getBoundSql(args[1]);
  //注意这里,是获取 pageStartRow 和  pageSize
                args[1] = parser.setPageParameter(ms, args[1], boundSql, page); 
              
  //之前已经执行过count()查询
                page.setCountSignal(Boolean.FALSE);
                //执行分页查询
                Object result = invocation.proceed();
                //得到处理结果
                page.addAll((List) result);
            }
        } finally {
            ((PageSqlSource)ms.getSqlSource()).removeParser();
        }

        //返回结果
        return page;
    }
  1. 先查判断是否查总数,具体是通过page.isCount(), 这个参数我们在前面默认设置为true,
  2. 最后将执行的结果放进Page,page也是 ArrayList

题外

这里注意下

image-20200530122930469

这个方法使用了桥接模式,抽象类实现了基本的方法

image-20200530123041111

每个数据库继承了此类,实现了自己的方法

image-20200530123219847

我们来看下 MysqlParser

image-20200530123335016

主要是设置参数,和分页查询sql。

小节

PageHelper的原理主要是 mybatis拦截器

查总数用 select count(0) from ( 自己的sql)

分页用 select ---- limit ?,? 实现分页

后面放到 PageInfo里面去

### PageHelper 分页插件使用教程 #### 导入依赖 为了在项目中使用PageHelper分页插件,首先需要导入相应的依赖。对于Maven项目来说,可以在`pom.xml`文件中加入如下配置来引入PageHelper: ```xml <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.8</version> </dependency> ``` 此操作会自动下载并集成所需的库到工程环境中[^4]。 #### 插件初始化设置 接着,在Spring Boot应用程序或其他基于Spring框架的应用程序里,可以通过Java配置类或者XML形式注册该插件。通常推荐采用更简洁的Java Config方式来进行全局配置: ```java @Configuration public class MyBatisConfig { @Bean public PageHelper pageHelper() { PageHelper pageHelper = new PageHelper(); Properties p = new Properties(); p.setProperty("offsetAsPageNum", "true"); p.setProperty("rowBoundsWithCount", "true"); p.setProperty("reasonable", "true"); pageHelper.setProperties(p); return pageHelper; } } ``` 上述代码片段展示了如何创建一个自定义配置实例,并设置了几个常用的属性选项以优化性能表现和用户体验[^2]。 #### 实现基本分页逻辑 当完成了前期准备工作之后,便可以着手编写具体的业务处理函数了。下面给出了一段典型的DAO层接口实现示例,用于执行带有限制条件的数据检索请求: ```java // 假设有一个名为UserMapper.java 的 Mapper 接口 @Select("SELECT * FROM users WHERE status=#{status}") List<User> selectUsersByStatus(@Param("status") Integer status); // 对应的服务端控制器方法内调用 mapper 方法前加上分页语句即可轻松达成目的 int pageNum = 1; // 当前页面编号,默认第一页 int pageSize = 10; // 每页显示记录数 PageHelper.startPage(pageNum, pageSize); List<User> userList = userMapper.selectUsersByStatus(1); // 正常查询列表数据 System.out.println(new PageInfo<>(userList).getTotal()); // 输出总条目数量 ``` 这里的关键在于调用了静态工具类 `PageHelper.startPage()` 来指定当前要展示哪一部分的内容范围;随后正常发起SQL指令读取目标表中的信息集合;最后借助于辅助对象 `PageInfo<T>` 可方便地获取有关本次查询的一些统计指标,比如总数、是否有下一页等附加元数据[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值