PageHelper分页插件使用 及原理

1. PageHelper简介

一款很好用的 分页插件,支持多种数据库,拿来即用

2.环境

springboot 2.7.1、 jdk11、pagehelper1.4.2

3. PageHelper 使用

3.1 导包

<!-- 版本需要1.4 以及以上 否则会报错 --> 
<dependency>
	<groupId>com.github.pagehelper</groupId>
	<artifactId>pagehelper-spring-boot-starter</artifactId>
	<version>1.4.1</version>
</dependency>

启动报错: 使用1.4 以下版本,会报如下错
PageHelper分页插件使用 及原理_第1张图片
解决办法:二选一即可,推荐第一种

  1. 升高 pageHelper版本到1.4.1 及以上
<dependency>
	<groupId>com.github.pagehelper</groupId>
	<artifactId>pagehelper-spring-boot-starter</artifactId>
	<version>1.4.1</version>
</dependency>
  1. 如下配置
	spring:
 		 main:
    		allow-circular-references: true

3.2 简单使用

	// pageNum 当前页数, pageSize 每页条数;
	/* 
		注意:  
			1. 只有紧跟在PageHelper.startPage方法后的第一个Mybatis的查询(Select)方法会被分页。
			2. 由于嵌套结果方式会导致结果集被折叠,因此分页查询的结果在折叠后总数会减少,所以无法保证分页结果数量正确。
	*/
	PageHelper.startPage(pageNum, pageSize);
	List<SysUser1> list1 = sysUser1Mapper.example();

3.3 其它使用方式

	   // 方式1
        PageHelper.startPage(1, 1);
        List<SysUser1> list1 = sysUser1Mapper.example();
        System.out.println("方式1: " + list1);
        System.out.println();

        // 方式二
        PageHelper.offsetPage(1, 1);
        List<SysUser1> list2 = sysUser1Mapper.example();
        System.out.println("方式2: " + list2);
        System.out.println();


        // 方式三
        List<SysUser1> list3 = session.selectList("com.example.demoproject.server.dao.SysUser1Mapper.list", null, new PageRowBounds(1, 1));
        System.out.println("方式3: " + list3);
        System.out.println();


        /*
            方式四,参数方法调用
                1. 接口方法定义, xml 中不需要处理这两个参数  List selectByPageNumSize(@Param("pageNum") int pageNum, @Param("pageSize") int pageSize);
                2. supportMethodsArguments=true
         */
        List<SysUser1> list4 = sysUser1Mapper.selectByPageNumSize(1, 1);
        System.out.println("方式4: " + list4);
        System.out.println();


        /*
            第五种
                1. 参数对象 com.example.demoproject.server.common.req.BaseReq 属性有 pageNum 和 pageSize 只要参数有值,也会被分页
                2. supportMethodsArguments=true
         */
        BaseReq baseReq = new BaseReq();
        baseReq.setPageNum(1);
        baseReq.setPageSize(1);
        List<SysUser1> list5 = sysUser1Mapper.selectByPageNumSize(baseReq);
        System.out.println("方式5: " + list5);
        System.out.println();


        /*
            第六种,ISelect 接口方式; 也可以 lambda表达式方式
             6.1 doSelectPage
             6.2 doSelectPageInfo
             6.3 PageHelper.count
         */
        // 6.1 doSelectPage
        Page<SysUser1> page = PageHelper.startPage(1, 1).doSelectPage(new ISelect() {
            @Override
            public void doSelect() {
                List<SysUser1> list6 = sysUser1Mapper.example();
                System.out.println("方式6-doSelectPage-1: " + list6);
                System.out.println();
            }
        });
        System.out.println("方式6-doSelectPage-2: " + page);
        System.out.println();


        // 6.2 doSelectPageInfo
        PageInfo<SysUser1> objectPageInfo = PageHelper.startPage(1, 1).doSelectPageInfo(new ISelect() {
            @Override
            public void doSelect() {
                List<SysUser1> list6 = sysUser1Mapper.example();
                System.out.println("方式6-doSelectPageInfo-1: " + list6);
                System.out.println();
            }
        });
        System.out.println("方式6-doSelectPageInfo-2: " + objectPageInfo);
        System.out.println();

        // 6.3 PageHelper.count
        long total = PageHelper.count(new ISelect() {
            @Override
            public void doSelect() {
                List<SysUser1> list6 = sysUser1Mapper.example();
                System.out.println("方式6-count-1: " + list6);
                System.out.println();
            }
        });
        System.out.println("方式6-total-2: " + total);
        System.out.println();

3.4 配置项(一般配置在 yml后缀文件中)

#格式:  
# pagehelper:
#	xxxxx: xx
# 如:
# pagehelper:
#   offsetAsPageNum: false 

# ############################常用配置####################################################
# helperDialect  数据库方言, 默认:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式
# 可选择 oracle,mysql,postgresql,db2,sqlserver,sqlserver2012 等
# 特别注意: 使用 SqlServer2012 数据库时, 需要手动指定为 sqlserver2012, 否则会使用 SqlServer2005 的方式进行分页


# offsetAsPageNum:默认值为 false, 该参数对使用 RowBounds 作为分页参数时有效。 当该参数设置为 true 时,会将 RowBounds 中的 offset 参数当成 pageNum 使用,可以用页码和页面大小两个参数进行分页。

# rowBoundsWithCount:默认值为false,该参数对使用 RowBounds 作为分页参数时有效。 当该参数设置为true时,使用 RowBounds 分页会进行 count 查询。

# pageSizeZero:默认值为 false,当该参数设置为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型)。

# reasonable:分页合理化参数,默认值为false。当该参数设置为 true 时,pageNum<=0 时会查询第一页, pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询。

# params:为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值, 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero。

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


# ############################不常用配置####################################################
# autoRuntimeDialect:默认值为 false。设置为 true 时,允许在运行时根据多数据源自动识别对应方言的分页 (不支持自动选择sqlserver2012,只能使用sqlserver)

# closeConn:默认值为 true。当使用运行时动态数据源或没有设置 helperDialect 属性自动获取数据库类型时,会自动获取一个数据库连接, 通过该属性来设置是否关闭获取的这个连接,默认true关闭,设置为 false 后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定。

配置项说明: springboot 会自动联想,此处注意如果联想出来不是驼峰命名,也不报错,但是参数会不生效
如:

配置说明

4. 原理

4.1 问题

  上述说的简单使用方式,只在要执行的sql之前加一行代码就行,那这行代码是怎么做到的呢?想搞清楚这个首先引出最简单的问题是分页参数怎么和sql执行关联,sql又是什么时候执行的?

4.2 分页参数设置原理

  我们可以跟踪进去看下发现代码是这样的,分两步,第一步是,分页参数包装为Page对象,第二步是,Page对象放入 ThreadLocal 中。

  ThreadLocal 是什么呢,简单说是 本地线程变量,存储了当前线程所需要的数据副本,只有当前线程可以使用,作为解决并发问题的一种实现(spring就是基于此)。也就是说,如果不手动清理的话,你放进去的数据,在线程销毁之前都能用,所以一般用这个的时候不用的数据要及时手动清理防止内存泄漏。

    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    	// 1.构建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());
        }
        // 2.page对象 放入 ThreadLocal 中
        setLocalPage(page);
        return page;
    }

4.3 分页实现

4.3.1 程序入口

  PageInterceptor.intercept 方法。首先看PageInterceptor这个类,拦截了两个方法,一个是四个参数,一个是六个参数的 实现类 org.apache.ibatis.plugin.Interceptor(mybatis提供的插件类)
pageInterceptor截图
  再看方法 intercept ,重写了mybatis提供的方法,核心统计代码如下,主要就是 skip方法 有三个实现类 根据不同配置有不同的实现,此处是PageHelper

	//调用方法判断是否需要进行分页,如果不需要,直接返回结果
	  if (!dialect.skip(ms, parameter, rowBounds)) {
	      //判断是否需要进行 count 查询(有了分页参数才会进来,此处不知道为什么还需要再次判断下)
	      if (dialect.beforeCount(ms, parameter, rowBounds)) {
	          //查询总数 此处就是查询核心代码
	          Long count = count(executor, ms, parameter, rowBounds, null, 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);
	  }

4.3.2 分页核心方法

  PageHelper.skip() 主要是 pageParams.getPage 获取分页信息

 @Override
    public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
        if (ms.getId().endsWith(MSUtils.COUNT)) {
            throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
        }
        // 获取分页信息
        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, page.getDialectClass());
            return false;
        }
    }

  PageParams.getPage(Object parameterObject, RowBounds rowBounds)重头戏来了,分页处理,处理完回到 skip 方法

public Page getPage(Object parameterObject, RowBounds rowBounds) {
		/*
		分页方式: PageHelper.startPage 、 RowBound 方式 、 supportMethodsArguments=true
		*/
        Page page = PageHelper.getLocalPage();
        if (page == null) {
        	// 2.没有主动设置分页 此处支持自己设置 分页参数时的逻辑此处用的是等等不是 equals
            if (rowBounds != RowBounds.DEFAULT) {
            	// 3. rowBounds分页逻辑
                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(parameterObject instanceof IPage || supportMethodsArguments){
            	// IPage 或 supportMethodsArguments 参数配置处理(入参里面拿分页参数封装为page对象)
                try {
                    page = PageObjectUtil.getPageFromObject(parameterObject, false);
                } catch (Exception e) {
                    return null;
                }
            }
            if(page == null){
                return null;
            }
            // 没有显示设置分页参数 且需要分页时 分页参数放入 ThreadLocal
            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;
    }

4.3.3 分页

   分页参数解析后,如果需要分页,则skip 校验通过,进入if逻辑 执行分页sql,然后在查询数据,最终将数据封装为 Page(实际是list)返回,详见 4.3.1 程序入口 注释说明

4.3.4 总结

  1. PageHelper是一款国人自己编写的分页插件,结合mybatis、 spring 使用
  2. 简单使用方式,只需要在需要执行的sql上面,加入代码 PageHelper.startPage(pageNum, pageSize); 即可实现
  3. 分页原理是 通过ThreadLocal 或者 开启自动查找参数中的分页参数,在执行真实sql 之前查询总数,然后返回Page(list)对象,实现分页效果

4.4 资源下载

文中所属项目下载地址: https://download.csdn.net/download/m0_46861007/86248872

你可能感兴趣的:(java,mybatis,spring,boot,后端,mysql)