乐趣区

关于spring:基于Mybatis的分页控制-PageHelper分页控制底层原理

分页是基于 WEB 的利用绕不开的话题,个别状况下基于 Mybatis 的 java 我的项目可选的分页计划包含:

  1. 开源我的项目 PageHelper。
  2. 基于 Mybatis 的 RowBounds 分页。
  3. Mybatis-plus 分页。
  4. 本人实现的分页计划。

明天咱们次要剖析前两种分页计划,Mybatis-plus 的分页放在下一篇文章中剖析。除非有非凡起因,个别状况下也不太倡议本人再去实现分页计划,因为无论是 PageHelper 还是 Mybatis-plus 的分页计划,绝大部分状况下也够用了,没有必要反复造轮子。

物理分页和逻辑分页

一般来讲,分页针对的是执行数据库查问的时候,符合条件的数据有很多、然而前端页面一次不须要展现全副数据的利用场景。

在这一场景下,应用层获取当前页数据的计划天然就分为两种:一种是应用层向数据库获取所有满足条件的数据,而后在应用层内存中对后果集进行过滤、获取到当前页数据后返回给前端。另一种须要数据库的反对,应用层只向数据库申请当前页的数据、获取数据后间接返回给前端。

第一种形式就是咱们常说的 逻辑分页,也能够叫内存分页。第二种形式就是 物理分页。

两种形式的区别其实高深莫测,逻辑分页不止是对数据库有内存、性能的压力,而且对于网络传输、应用层内存都会存在性能压力。尤其是在满足条件的数据量特地大(比方 10w 条)、而当前页须要的数据量比拟小(个别状况下都会比拟小,比方 10 条)的状况下,应用层获取到的绝大部分数据都被抛弃了,所以对于数据库服务器内存、网络、应用服务器内存都是一种极大地节约。

而物理分页因为从数据库获取到的数据量比拟小,所以性能压力比拟小。

因而,正式我的项目即便后期可能判断未来数据量不会太大的状况下,也不倡议应用逻辑分页计划。

当然,物理分页须要数据库的反对,比方 MySQL 的 limit,Oracle 的 rownum 等等,目前大部分的支流数据库都能够提供相似的反对。

基于 Mybatis 的 RowBounds 的分页实现

Mybatis 内置提供了基于 RowBounds 的分页计划,只有咱们在 mapper 接口中提供 RowBounds 参数,Mybatis 天然就能够帮咱们实现分页。

然而咱们必须要晓得,RowBounds 是逻辑分页!所以咱们用学习理解的态度来钻研一下 RowBounds 分页计划的实现机制,我的项目中不倡议间接应用。

基于 RowBounds 的分页实现非常简单,只有在 mapper 接口中设置 RowBounds 参数即可,比方获取所有用户的接口:

List<User> selectPagedAllUsers(RowBounds rowBounds);

List<User> selectAllUsers();

在 mapper.xml 文件中上述两个接口对应的 sql 语句能够齐全一样,selectPagedAllUsers 是实现分页的接口,selectAllUsers 是不分页的接口。

那么咱们应该怎么传递 RowBounds 呢?首先要理解一下 RowBounds 具体是个什么东东。

其实 RowBounds 的定义很简略,最重要的两个参数,offset 其实就是起始地位,limit 能够了解为每页行数。

 private final int offset;
 private final int limit;

  public RowBounds(int offset, int limit) {
    this.offset = offset;
    this.limit = limit;
  }

转换为咱们比拟相熟的概念,currentPage 示意当前页,pageSize 示意每页行数,则 Dao 层获取分页数据的办法能够这么写:

      
public List<User> getPagedUser(int currentPage,    int pageSize){int offset=(currentPage - 1) * pageSize;
    RowBounds rowBounds=new RowBounds(offset,pageSize);
    return UserMapper.selectPagedAllUsers(rowBounds);
}

你能够找一个数据量比拟大的表试一下,性能是能够直观感触到的(慢)。

PageHelper 的分页原理

PageHelper 是利用 Mybatis 拦截器实现分页的,他的基本原理是:

  1. 应用层在须要分页的查问执行前,设置分页参数。
  2. 应用 Mybatis 的 Executor 拦截器拦挡所有的 query 申请。
  3. 在拦截器中查看以后申请是否设置了分页参数,没有设置分页参数则执行原查问返回所有后果。
  4. 如果以后查问设置了分页参数,则执行分页查问:依据数据库类型革新以后的查问 sql 语句,减少获取当前页数据的 sql 参数,比方对于 mysql 数据库,在 sql 语句中减少 limit 语句。
  5. 执行革新后的分页查问,获取数据返回。

PageHelper 分页的实现 #RowBounds 形式

Springboot 我的项目利用 PageHelper 实现分页非常简单,pom.xml 中引入依赖即可:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.1</version>
</dependency>

除了咱们后面提到过的,通过 PageHelper.startpage()设置分页参数之外,PageHelper 还有另外一个设置分页参数的办法就是通过 RowBounds。

通过 RowBounds 进行分页参数的设置从而实现分页的形式这种状况下参数 offset-as-page-num 会失效,设置为 true 的话代表 RwoBounds 的 offset 会作为 pageNum,limt 会作为 pageSize 参数应用。

引入 PageHelper 之后,再跑上一节 RowBounds 的那个例子,就会发现分页最终是变成了通过 PageHelper 实现、而非 Mybatis 原生的 RowBounds 实现的。

如果加上咱们后面文章说过的打印 sql 语句、统计 sql 执行时长的 Mybatis 拦截器(必须确保打印 sql 语句的拦截器在 PageHelper 拦截器之前初始化),会发现打印进去的 sql 语句多了 limit 语句。而且,如果咱们是对数据量比拟大(比方 10w 条数据)的表执行分页查问的话(比方查问最初一页),会发现通过 Mybatis 原生 RowBounds 实现的分页查问要比 PageHelper 的 RowBounds 形式慢很多。

PageHelper#RowBounds 形式的实现原理

PageHelper 的源码不算简单,跟踪一下就能够发现应用 RowBounds 传递分页参数的底层原理。

PageHelper 分页拦截器执行过程中会调用到 PageParameters 的 getPage 办法:

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(parameterObject instanceof IPage || 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;
    }

能够看到首先还是要去获取通过 startPage 设置的分页参数,如果获取到的话就不会管 RowBounds 了。

所以咱们要晓得 PageHelper 还是以 startPage 优先的。

否则如果没有通过 startPage 办法设置分页参数,零碎就会用 rowBounds 的 offset 和 limit 作为分页参数去 new 一个 page 对象,同时把 page 对象设置到 PageHelper 的 LocalPage 中并返回,PageHelper 的 LocatPage 是 ThreadLocal 变量,把新创建的 page 对象保留在 PageHelper 的 LocalPage 中其实就是模仿了 startPage 的操作,这个操作之后,rowBounds 形式传递分页参数就和 startPage 形式走到同一条路线下来了。

PageHelper#startPage 形式

执行分页查问前,应用层调用 PageHelper 的 startPage 设置分页参数的形式。

startPage 应用了 ThreadLocal 对象,ThreadLocal 是线程级参数,为了防止同一申请线程中的多个查问语句的分页管制不造成相互影响,要求应用层必须正好在须要分页的查问执行前后设置、清空分页参数。

集体认为这是对 PageHelper#startPage 形式下的一个限度条件,咱们在应用 PageHelper 的时候必须留神、做好管制,否则可能会有意想不到的结果、或者导致利用的 bug。

比方,咱们要获取到用户 A 有权限的所有销售订单数据,这个过程中可能至多要有两条 sql 语句要执行,第一条 sql 语句获取用户 A 的权限,第二条 sql 依据用户 A 的权限获取销售订单数据,咱们须要对第二条获取销售订单数据的 sql 语句进行分页,伪代码如下:

PageHelper.startPage(1,10);
permission.get(userA); // 执行获取用户 A 权限的 sql
sellOrder.getOrder(); // 执行获取订单数据的 sql

以上伪代码会导致获取用户权限的调用也被分页,这并不是咱们想要的后果(也可能会导致 bug)。而正确的伪代码应该是:

permission.get(userA); // 执行获取用户 A 权限的 sql
PageHelper.startPage(1,10);
sellOrder.getOrder(); // 执行获取订单数据的 sql
PageHelper.clearPage();

须要留神的不仅仅是 startPage 的地位,而且还必须要有 clearPage 的调用,否则,如果不调用 clearPage 清理分页参数的话,以后交易的后续其余 sql 也会被影响。

咱们从 PageHelper 的源码角度剖析一下 startPage 的实现原理。

先看一下 startPage 干了啥:以 pageNum 和 pageSize 为参数创立 Page 对象并调用 setLocalPage 办法。而 setLocalPage 办法就是把创立的 Page 对象存储在 ThreadLocal 对象中。

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.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        // 当曾经执行过 orderBy 的时候
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {page.setOrderBy(oldPage.getOrderBy());
        }
        setLocalPage(page);
        return page;
    }

之后在执行查问语句的时候,PageHelper 的 Executor 拦截器 PageInterceptor 拦挡到 query 办法后,通过 ThreadLocal 变量就能获取到 Page 对象从而获取到分页参数,之后就能够从新加工查问的 sql 语句减少 limit 语句从而实现分页。

PageHelper#startPage 测试

还是在后面无关 Mybatis 的相干文章中用过的 demo 的根底上做测试,拦截器也都保留。

controller 减少一个办法:

   @ResponseBody
    @RequestMapping("allsellforms")
    public List<SellForm> allSellforms(int pageIndex,int pageSize){return sellFormService.getAllSellforms(pageIndex,pageSize);
    }

service 层代码省略,调用到 dao 层(也能够不要 Dao,间接在 service 中调用 mapper 接口办法):

    public List<SellForm> fetchSellForms(int pageIndex,int pageSize) {//RowBounds rowBounds=new RowBounds(pageIndex,pageSize);
        //return sellFormMapper.selectSellForms(rowBounds);
        PageHelper.startPage(pageIndex,pageSize);
        List<SellForm> sellForms=sellFormMapper.selectSellForms();
        PageHelper.clearPage();
        return sellForms;
    }

用 controller 传递进来的 pageIndex 和 pageSize 参数调用 startPage, 而后执行查问,记得执行查问之后再通过 clearPage 清理分页参数。

PageHelper#mapper 接口传递分页参数

能够通过 mapper 接口参数来传递分页参数从而实现分页。而且 PageHelper 能够反对两种参数传递的形式:

  1. 通过实现了 IPage 接口的对象传递分页信息。
  2. 设置 methods-arguments 为 true,并设置 param 参数或者应用默认参数名传递分页信息。

IPage 接口:实现 IPage 接口后,PageHelper 通过 IPage 接口的 getPageNum()、getPageSize()来获取分页参数。

methods-arguments:设置为 true 后,能够通过 param 参数指定 pageNum、PageSize 等分页信息的属性名称,也能够不指定,则零碎采纳默认的属性获取分页信息:

pageNum=pageNum;
pageSize=pageSize;
count=countSql;
reasonable=reasonable;
pageSizeZero=pageSizeZero;

小结

Mybatis 我的项目的分页应该是一个刚性需要,绝大部分的我的项目都须要,PageHelper 在绝大部分状况下应该也足够反对我的项目的分页需要了。

PageHelper 提供了非常灵活的分页参数传递形式,其中通过 startPage 传递分页参数的形式存在肯定的限度、某些场景下使用不当可能会导致意想不到的问题,在应用过程中肯定要留神。

startPage 形式存在的限度,能够在我的项目中想方法解决,比方能够应用 IPage 接口的形式、参数对象的形式,也能够在 startPage 的根底上进行肯定的封装,比方能够想方法将分页信息和 MapperdStatement 的 id 进行匹配或绑定等等,总之 PageHelper 曾经提供了肯定的灵活性,咱们在我的项目上能够依据具体情况进行肯定的扩大,实现取长补短灵便利用。

以上!

上一篇 Mybatis 拦截器程序

退出移动版