PageHelper 分页插件详解

5,409 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

PageHelper 的使用

非 SpringBoot 项目中使用

① 引入依赖

<!-- pageHelper 分页插件 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.0.0</version>
</dependency>

② 插件设置

XML 方式 : MyBatis.xml

<configuration>
    ...
    <!-- 使用插件 -->
    <plugins>
        <!-- 使用分页插件 -->
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 分页参数合理化, 当页码数 < 0 时, 显示第一页.   当页码数 > 总页码数时, 显示最后一页  -->
            <property name="reasonable" value="true"/>
        </plugin>
    </plugins>
</configuration>

JavaConfig 方式 :  添加配置类 :

@Configuration
public class PageHelperConfig {
    @Bean
    public PageHelper pageHelper(){
        PageHelper pageHelper = new PageHelper();
        Properties p = new Properties();
        //1.offsetAsPageNum:设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用.
        p.setProperty("offsetAsPageNum", "true");
        //2.rowBoundsWithCount:设置为true时,使用RowBounds分页会进行count查询.
        p.setProperty("rowBoundsWithCount", "true");
        //3.reasonable:启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页。
        p.setProperty("reasonable", "true");
        pageHelper.setProperties(p);
        return pageHelper;
    }
}

③  Service 层

@Service
public class EmployeeService {

     @Autowired
     EmployeeMapper employeeMapper;
     
     public List<Employee> getAll(){
           EmployeeExample example = new EmployeeExample();
           example.setOrderByClause("emp_id asc");
           return employeeMapper.selectByExampleWithDept(example);
     }
}

④ Controller 层 :

    // 查询所有 Employee , 分页查询
    @RequestMapping("/emps")
    public String getEmps( @RequestParam(value = "pn", defaultValue = "1") Integer pn, Model model) {
        System.out.println("请求参数 pn = " + pn);
        // 分页查询, 可以引入 PageHelper 分页插件. 在查询之前, 只需要调用 startPage() 方法即可.
        // 参数1: 查询开始的页码 . 参数2:每页的大小.
        PageHelper.startPage(pn, 5);
        // startPage() 方法后面紧跟的这个查询就是一个分页查询.
        List<Employee> employees = employeeService.getAll();
        // 使用 pageInfo 包装查询后的结果, 封装了十分详细的分页信息, 包括有查询出来的数据, 传入连续显示的页数, .....
        // 参数1: 要显示的数据. 参数2: 页码下标连续显示几页.
        PageInfo<Employee> page = new PageInfo<>(employees, 5);
        model.addAttribute("pageInfo", page);
        return "list";
    }

SpringBoot 项目中使用

使用方式大同小略, 修改一下依赖和配置即可.

① 引入依赖

<!-- pageHelper 分页插件, SpringBoot 项目中使用 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.10</version>
</dependency>

② 配置 application.yml

# 分页配置
pagehelper:
  helper-dialect: mysql	#使用的数据库. mysql, oracle, sqlite
  reasonable: true #开启优化,在分页页码结果没有数据的时候,会显示有数据的页码数据,也就是当当前页<1时,返回第 1 页, 当当前页 > 最大页时, 返回最后一页的数据.
  support-methods-arguments: true #是否支持接口参数来传递分页参数,默认false
  pageSizeZero: false #表示当 pageSize=0 时返回所有
  params: count=countSql

③ 使用

Controller :

    /**
     * 分页获取任务执行结果
     * @return List<TaskResultVo>
     */
    @GetMapping(value = "page/{pageSize}/{pageNum}")
    public Result page(@PathVariable(value = "pageSize") Integer pageSize,
                       @PathVariable(value = "pageNum") Integer pageNum) {
        PageHelper.startPage(pageNum,pageSize);
        List<TaskResult> taskResultList = taskResultService.list();
        PageInfo<TaskResult> page = new PageInfo<>(taskResultList, 5);
        return new Result(true,200,"查询成功", page);
    }

Service

    /**
     * 获取所有的任务执行结果
     * @return List<TaskResult>
     */
    public List<TaskResult> list() {
        TaskResultExample taskResultExample = new TaskResultExample();
        taskResultExample.setOrderByClause("id");
        return taskResultMapper.selectByExample(taskResultExample);
    }

分页类 PageInfo 解释

分页类 PageInfo 的所有属性 :

    private int pageNum;       // 当前页的号码
    private int pageSize;      // 每页的数量
    private int size;          // 当前页的数量
    private String orderBy;    // 排序
    //由于startRow和endRow不常用,这里说个具体的用法
    //可以在页面中"显示startRow到endRow 共size条数据"
    private int startRow;    // 当前页面第一个元素在数据库中的行号
    private int endRow;      // 当前页面最后一个元素在数据库中的行号
    private long total;        // 总记录数
    private int pages;         // 总页数
    private List<T> list;      // 结果集
    private int firstPage;     // 第一页
    private int prePage;       // 前一页
    private int nextPage;      // 下一页
    private int lastPage;      // 最后一页
    private boolean isFirstPage = false;        // 是否为第一页
    private boolean isLastPage = false;         // 是否为最后一页
    private boolean hasPreviousPage = false;    // 是否有前一页
    private boolean hasNextPage = false;        // 是否有下一页
    private int navigatePages;                  // 导航页码数
    private int[] navigatepageNums;             // 所有导航页号

分页插件需要注意的问题

分页的格式要求比较高 , 如下 :

  • 使用 PageHelper.startPage(pageNum,pageSize);  开启分页
  • 中间有个获取数据的方法
  • 创建 PageInfo pageInfo = new PageInfo<>(taskResultList);    分页对象

关键点在于 中间获取数据的方法, 该方法很容易导致分页返回的总记录数 total 错误.

① 如果中间获取数据的方法, 只是一个简单的从数据库查询就返回, 没有经过其他任何操作的话, 分页是没有问题的:

/**
* 分页获取任务执行结果
* @return List<TaskResult>
*/
@GetMapping(value = "page/{pageSize}/{pageNum}")
    public Result page(@PathVariable(value = "pageSize") Integer pageSize,
                       @PathVariable(value = "pageNum") Integer pageNum) {
    PageHelper.startPage(pageNum,pageSize);     //开启分页
    List<TaskResult> taskResultList = taskResultService.list();	//获取数据
    PageInfo pageInfo = new PageInfo<>(taskResultList);      //创建分页对象
    return new Result(true,200,"查询成功", pageInfo);
}

/**
* 获取所有的任务执行结果
* @return List<TaskResult>
*/
public List<TaskResult> list() {
    TaskResultExample taskResultExample = new TaskResultExample();
    taskResultExample.setOrderByClause("id");
    return taskResultMapper.selectByExample(taskResultExample);
}

② 如果中间获取数据的方法, 从数据库查询数据后, 还还要对查询的数据进行处理, 那么分页结果会有问题的 :

错误使用 :

    /**
     * 分页获取任务执行结果
     * @return List<TaskResultVo>
     */
    @GetMapping(value = "page/{pageSize}/{pageNum}")
    public Result page(@PathVariable(value = "pageSize") Integer pageSize,
                       @PathVariable(value = "pageNum") Integer pageNum) {
        PageHelper.startPage(pageNum,pageSize);     //开启分页
        List<TaskResultVo> taskResultVoList = taskResultService.listVo();	//获取数据
        PageInfo pageInfo = new PageInfo<>(taskResultVoList);      //创建分页对象
        return new Result(true,200,"查询成功", pageInfo);
    }

    /**
     * 获取所有的任务执行结果
     * @return List<TaskResult>
     */
    public List<TaskResultVo> listVo() {
        TaskResultExample taskResultExample = new TaskResultExample();
        taskResultExample.setOrderByClause("id");
        List<TaskResult> taskResultList = taskResultMapper.selectByExample(taskResultExample);
        ArrayList<TaskResultVo> taskResultVos = new ArrayList<>();
        //获取数据后,进行了处理然后才返回的
        taskResultList.forEach(x -> {
            TaskResultVo taskResultVo = TaskResultVo.convertToTaskResultVo(x);
            taskResultVos.add(taskResultVo);
        });
        return taskResultVos;
    }

但是从数据库获取到的数据, 必须进行处理, 那该怎么办呢?

正确的使用方式 :

===Controller
	/**
     * 分页获取任务执行结果
     * @return List<TaskResultVo>
     */
    @GetMapping(value = "page/{pageSize}/{pageNum}")
    public Result page(@PathVariable(value = "pageSize") Integer pageSize,
                       @PathVariable(value = "pageNum") Integer pageNum) {
        PageHelper.startPage(pageNum,pageSize);     //分页
        List<TaskResult> taskResultList = taskResultService.list();
        PageInfo pageInfoOld = new PageInfo<>(taskResultList);  //先保存分页结果
        List<TaskResultVo> taskResultVoList = taskResultService.listVo(taskResultList);     //结果集做其他操作
        PageInfo<TaskResultVo> pageInfoNew = new PageInfo<>(taskResultVoList);      //再次创建分页对象
        BeanUtils.copyProperties(pageInfoOld,pageInfoNew,"list");   //复制属性(Spring的)
        return new Result(true,200,"查询成功", pageInfoNew);
    }
    
===Service
    /**
     * 获取所有的任务执行结果
     * @return List<TaskResult>
     */
    public List<TaskResult> list() {
        TaskResultExample taskResultExample = new TaskResultExample();
        taskResultExample.setOrderByClause("id");
        return taskResultMapper.selectByExample(taskResultExample);
    }

    /**
     * 获取所有的任务执行结果
     * @return List<TaskResultVo>
     */
    public List<TaskResultVo> listVo(List<TaskResult> list) {
        ArrayList<TaskResultVo> taskResultVos = new ArrayList<>();
        list.forEach(x -> {
            TaskResultVo taskResultVo = TaskResultVo.convertToTaskResultVo(x);
            taskResultVos.add(taskResultVo);
        });
        return taskResultVos;
    }

不生效的可能原因

1.版本过低

详见:blog.csdn.net/linxinglian…

2.语句顺序不对

PageHelper 里面的 PageHelper.startPage(1,10); 只对该语句以后的第一个查询语句得到的数据进行分页.

比如:

PageHelper.startPage(pageNum, pageSize);
List<BrandBo> brands= brandMapper.getBrand();

3.业务顺序问题

比如,如下的代码,分页是存在问题的:

PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

PageHelper 方法使用了 ThreadLocal 参数,分页参数和线程是绑定的。

只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的,因为 PageHelper 在 finally 代码段中会自动 remove 掉 ThreadLocal 存储的对象。

那么对于上面的代码,如果 param1 == null 时,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上,不会被 remove 掉。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

所以正确的写法应该是下面这样:

List<Country> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

4.配置了多个分页插件

系统中配置了多个分页插件,导致异常。

5.不支持带有 for update 语句的分页

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

6.左连接不分页问题

详见:cloud.tencent.com/developer/a…

分页插件性能问题

PageHelper 的分页功能是通过 Limit 拼接 SQL 实现的,在大数据量下,这种 limit 分页本来就有性能问题,所以 PageHelper 也就存在了性能问题。

解决办法:手写分页 sql,这就是深度分页问题了。

源码

关键词:ThreadLocal,MyBatis 拦截器

调用 startPage() 方法进行分页时,最终调用的方法如下:

里边最重要的就是 getLocalPage() 和 setLocalPage(page) 方法,他俩是看当前线程中的 ThreadLocal.ThreadLocalMap 中是否存在该 page 对象,若存在直接取出,若不存在则设置一个,我们以第一个为例继续深入。

一说到 sql 的拦截功能,大家应该会想到 Mybatis 的拦截器吧。PageHelper 也是用的 mybatis 的拦截器进行分页的。直接搜索PageInterceptor。代码也比较简单,略过。