关于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拦截器程序

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据