##什么是PageHelper

如果你也在用 MyBatis,建议尝试该分页插件,这一定是最方便使用的分页插件。分页插件支持任何复杂的单表、多表分页。

Maven

<dependency>
  <groupId>com.github.pagehelper</groupId>
  <artifactId>pagehelper</artifactId>
  <version>4.1.4</version>
</dependency>

配置拦截器

拦截器有两种方式:一种是Mybatis,一种是Spring

1、Spring方式

		<plugin interceptor="com.github.pagehelper.PageHelper">
			<!--指明数据库 4.0.0以后不需要设置此属性-->
			<!--<property name="dialect" value="mysql"/>-->
			<!-- 该参数默认为false -->
			<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
			<!-- 和startPage中的pageNum效果一样-->
			<property name="offsetAsPageNum" value="false"/>
			<!-- 该参数默认为false -->
			<!-- 设置为true时,使用RowBounds分页会进行count查询 -->
			<property name="rowBoundsWithCount" value="true"/>
			<!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
			<!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
			<property name="pageSizeZero" value="true"/>
			<!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
			<!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
			<!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
			<property name="reasonable" value="true"/>
			<!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
			<!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
			<!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
			<!-- 不理解该含义的前提下,不要随便复制该配置 -->
			<property name="params" value="pageNum=start;pageSize=limit;"/>
			<!-- 支持通过Mapper接口参数来传递分页参数 -->
			<property name="supportMethodsArguments" value="true"/>
			<!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
			<property name="returnPageInfo" value="check"/>
		</plugin>

2、Spring加载方式

<!--
    plugins在配置文件中的位置必须符合要求,否则会报错,顺序如下:
    properties?, settings?,
    typeAliases?, typeHandlers?,
    objectFactory?,objectWrapperFactory?,
    plugins?,
    environments?, databaseIdProvider?, mappers?
-->
<plugins>
    <!-- com.github.pagehelper为PageHelper类所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
        <property name="param1" value="value1"/>
	</plugin>
</plugins>

参数介绍

分页插件可选参数如下:

  • dialect:默认情况下会使用 PageHelper 方式进行分页,如果想要实现自己的分页逻辑,可以实现 Dialect(com.github.pagehelper.Dialect) 接口,然后配置该属性为实现类的全限定名称。

    >   <property name="dialectAlias" value="oracle=com.github.pagehelper.dialect.helper.OracleDialect"/>
    >   ```
    >
    >   
    >
    > **下面几个参数都是针对默认 dialect 情况下的参数。使用自定义 dialect 实现时,下面的参数没有任何作用。**
    >
    > 1. `helperDialect`:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。 你可以配置`helperDialect`属性来指定分页插件使用哪种方言。配置时,可以使用下面的缩写值:
    >    `oracle`,`mysql`,`mariadb`,`sqlite`,`hsqldb`,`postgresql`,`db2`,`sqlserver`,`informix`,`h2`,`sqlserver2012`,`derby`
    >    **特别注意:**使用 SqlServer2012 数据库时,需要手动指定为 `sqlserver2012`,否则会使用 SqlServer2005 的方式进行分页。
    >    你也可以实现 `AbstractHelperDialect`,然后配置该属性为实现类的全限定名称即可使用自定义的实现方法。
    > 2. `offsetAsPageNum`:默认值为 `false`,该参数对使用 `RowBounds` 作为分页参数时有效。 当该参数设置为 `true` 时,会将 `RowBounds` 中的 `offset` 参数当成 `pageNum` 使用,可以用页码和页面大小两个参数进行分页。
    > 3. `rowBoundsWithCount`:默认值为`false`,该参数对使用 `RowBounds` 作为分页参数时有效。 当该参数设置为`true`时,使用 `RowBounds` 分页会进行 count 查询。
    > 4. `pageSizeZero`:默认值为 `false`,当该参数设置为 `true` 时,如果 `pageSize=0` 或者 `RowBounds.limit = 0` 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 `Page` 类型)。
    > 5. `reasonable`:分页合理化参数,默认值为`false`。当该参数设置为 `true` 时,`pageNum<=0` 时会查询第一页, `pageNum>pages`(超过总数时),会查询最后一页。默认`false` 时,直接根据参数进行查询。
    > 6. `params`:为了支持`startPage(Object params)`方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置 `pageNum,pageSize,count,pageSizeZero,reasonable`,不配置映射的用默认值, 默认值为`pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero`。
    > 7. `supportMethodsArguments`:支持通过 Mapper 接口参数来传递分页参数,默认值`false`,分页插件会从查询方法的参数值中,自动根据上面 `params` 配置的字段中取值,查找到合适的值时就会自动分页。 使用方法可以参考测试代码中的 `com.github.pagehelper.test.basic` 包下的 `ArgumentsMapTest` 和 `ArgumentsObjTest`。
    > 8. `autoRuntimeDialect`:默认值为 `false`。设置为 `true` 时,允许在运行时根据多数据源自动识别对应方言的分页 (不支持自动选择`sqlserver2012`,只能使用`sqlserver`),用法和注意事项参考下面的**场景五**。
    > 9. `closeConn`:默认值为 `true`。当使用运行时动态数据源或没有设置 `helperDialect` 属性自动获取数据库类型时,会自动获取一个数据库连接, 通过该属性来设置是否关闭获取的这个连接,默认`true`关闭,设置为 `false` 后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定。
    >
    > **重要提示:**
    >
    > 当 `offsetAsPageNum=false` 的时候,由于 `PageNum` 问题,`RowBounds`查询的时候 `reasonable` 会强制为 `false`。使用 `PageHelper.startPage` 方法不受影响。
    
    ###参数配置使用场景
    
    ##### 场景一
    
    如果你仍然在用类似ibatis式的命名空间调用方式,你也许会用到`rowBoundsWithCount`, 分页插件对`RowBounds`支持和 MyBatis 默认的方式是一致,默认情况下不会进行 count 查询,如果你想在分页查询时进行 count 查询, 以及使用更强大的 `PageInfo` 类,你需要设置该参数为 `true`。
    
    **注:** `PageRowBounds` 想要查询总数也需要配置该属性为 `true`。
    
    ##### 场景二
    
    如果你仍然在用类似ibatis式的命名空间调用方式,你觉得 `RowBounds` 中的两个参数 `offset,limit` 不如 `pageNum,pageSize` 容易理解, 你可以使用 `offsetAsPageNum` 参数,将该参数设置为 `true` 后,`offset`会当成 `pageNum` 使用,`limit` 和 `pageSize` 含义相同。
    
    ##### 场景三
    
    如果觉得某个地方使用分页后,你仍然想通过控制参数查询全部的结果,你可以配置 `pageSizeZero` 为 `true`, 配置后,当 `pageSize=0` 或者 `RowBounds.limit = 0` 就会查询出全部的结果。
    
    ##### 场景四
    
    如果你分页插件使用于类似分页查看列表式的数据,如新闻列表,软件列表, 你希望用户输入的页数不在合法范围(第一页到最后一页之外)时能够正确的响应到正确的结果页面, 那么你可以配置 `reasonable` 为 `true`,这时如果 `pageNum<=0` 会查询第一页,如果 `pageNum>总页数` 会查询最后一页。
    
    ##### 场景五
    
    如果你在 Spring 中配置了动态数据源,并且连接不同类型的数据库,这时你可以配置 `autoRuntimeDialect` 为 `true`,这样在使用不同数据源时,会使用匹配的分页进行查询。 这种情况下,你还需要特别注意 `closeConn` 参数,由于获取数据源类型会获取一个数据库连接,所以需要通过这个参数来控制获取连接后,是否关闭该连接。 默认为 `true`,有些数据库连接关闭后就没法进行后续的数据库操作。而有些数据库连接不关闭就会很快由于连接数用完而导致数据库无响应。所以在使用该功能时,特别需要注意你使用的数据源是否需要关闭数据库连接。
    
    当不使用动态数据源而只是自动获取 `helperDialect` 时,数据库连接只会获取一次,所以不需要担心占用的这一个连接是否会导致数据库出错,但是最好也根据数据源的特性选择是否关闭连接。
    
    ## 重要提示
    
    ### `PageHelper.startPage`方法重要提示
    
    只有紧跟在`PageHelper.startPage`方法后的**第一个**Mybatis的**查询(Select)**方法会被分页。
    
    ### 请不要配置多个分页插件
    
    请不要在系统中配置多个分页插件(使用Spring时,`mybatis-config.xml`和`Spring`配置方式,请选择其中一种,不要同时配置多个分页插件)!
    
    ### 分页插件不支持带有`for update`语句的分页
    
    对于带有`for update`的sql,会抛出运行时异常,对于这样的sql建议手动分页,毕竟这样的sql需要重视。
    
    ### 分页插件不支持嵌套结果映射
    
    由于嵌套结果方式会导致结果集被折叠,因此分页查询的结果在折叠后总数会减少,所以无法保证分页结果数量正确。
    
    分页插件支持以下几种调用方式:
    
    

    java

//第一种,RowBounds方式的调用 List list = sqlSession.selectList(“x.y.selectIf”, null, new RowBounds(0, 10));

//第二种,Mapper接口方式的调用,推荐这种使用方式。 PageHelper.startPage(1, 10); List list = countryMapper.selectIf(1);

//第三种,Mapper接口方式的调用,推荐这种使用方式。 PageHelper.offsetPage(1, 10); List list = countryMapper.selectIf(1);

//第四种,参数方法调用 //存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数 public interface CountryMapper { List selectByPageNumSize( @Param(“user”) User user, @Param(“pageNum”) int pageNum, @Param(“pageSize”) int pageSize); } //配置supportMethodsArguments=true //在代码中直接调用: List list = countryMapper.selectByPageNumSize(user, 1, 10);

//第五种,参数对象 //如果 pageNum 和 pageSize 存在于 User 对象中,只要参数有值,也会被分页 //有如下 User 对象 public class User { //其他fields //下面两个参数名和 params 配置的名字一致 private Integer pageNum; private Integer pageSize; } //存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数 public interface CountryMapper { List selectByPageNumSize(User user); } //当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页 List list = countryMapper.selectByPageNumSize(user);

//第六种,ISelect 接口方式 //jdk6,7用法,创建接口 Page page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() { @Override public void doSelect() { countryMapper.selectGroupBy(); } }); //jdk8 lambda用法 Page page = PageHelper.startPage(1, 10).doSelectPage(()-> countryMapper.selectGroupBy());

//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() { @Override public void doSelect() { countryMapper.selectGroupBy(); } }); //对应的lambda用法 pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> countryMapper.selectGroupBy());

//count查询,返回一个查询语句的count数 long total = PageHelper.count(new ISelect() { @Override public void doSelect() { countryMapper.selectLike(country); } }); //lambda total = PageHelper.count(()->countryMapper.selectLike(country));


下面对最常用的方式进行详细介绍

#### 1). RowBounds方式的调用

java List list = sqlSession.selectList(“x.y.selectIf”, null, new RowBounds(1, 10));


使用这种调用方式时,你可以使用RowBounds参数进行分页,这种方式侵入性最小,我们可以看到,通过RowBounds方式调用只是使用了这个参数,并没有增加其他任何内容。

分页插件检测到使用了RowBounds参数时,就会对该查询进行**物理分页**。

关于这种方式的调用,有两个特殊的参数是针对 `RowBounds` 的,你可以参看上面的 **场景一** 和 **场景二**

**注:**不只有命名空间方式可以用RowBounds,使用接口的时候也可以增加RowBounds参数,例如:

java //这种情况下也会进行物理分页查询 List selectAll(RowBounds rowBounds);


**注意:** 由于默认情况下的 `RowBounds` 无法获取查询总数,分页插件提供了一个继承自 `RowBounds` 的 `PageRowBounds`,这个对象中增加了 `total` 属性,执行分页查询后,可以从该属性得到查询总数。

#### 2). `PageHelper.startPage` 静态方法调用

除了 `PageHelper.startPage` 方法外,还提供了类似用法的 `PageHelper.offsetPage` 方法。

在你需要进行分页的 MyBatis 查询方法前调用 `PageHelper.startPage` 静态方法即可,紧跟在这个方法后的第一个**MyBatis 查询方法**会被进行分页。

##### 例一:

java //获取第1页,10条内容,默认查询总数count PageHelper.startPage(1, 10); //紧跟着的第一个select方法会被分页 List list = countryMapper.selectIf(1); assertEquals(2, list.get(0).getId()); assertEquals(10, list.size()); //分页时,实际返回的结果list类型是Page,如果想取出分页信息,需要强制转换为Page assertEquals(182, ((Page) list).getTotal());


##### 例二:

java //request: url?pageNum=1&pageSize=10 //支持 ServletRequest,Map,POJO 对象,需要配合 params 参数 PageHelper.startPage(request); //紧跟着的第一个select方法会被分页 List list = countryMapper.selectIf(1);

//后面的不会被分页,除非再次调用PageHelper.startPage List list2 = countryMapper.selectIf(null); //list1 assertEquals(2, list.get(0).getId()); assertEquals(10, list.size()); //分页时,实际返回的结果list类型是Page,如果想取出分页信息,需要强制转换为Page, //或者使用PageInfo类(下面的例子有介绍) assertEquals(182, ((Page) list).getTotal()); //list2 assertEquals(1, list2.get(0).getId()); assertEquals(182, list2.size());


##### 例三,使用`PageInfo`的用法:

java //获取第1页,10条内容,默认查询总数count PageHelper.startPage(1, 10); List list = countryMapper.selectAll(); //用PageInfo对结果进行包装 PageInfo page = new PageInfo(list); //测试PageInfo全部属性 //PageInfo包含了非常全面的分页属性 assertEquals(1, page.getPageNum()); assertEquals(10, page.getPageSize()); assertEquals(1, page.getStartRow()); assertEquals(10, page.getEndRow()); assertEquals(183, page.getTotal()); assertEquals(19, page.getPages()); assertEquals(1, page.getFirstPage()); assertEquals(8, page.getLastPage()); assertEquals(true, page.isFirstPage()); assertEquals(false, page.isLastPage()); assertEquals(false, page.isHasPreviousPage()); assertEquals(true, page.isHasNextPage());


#### 3). 使用参数方式

想要使用参数方式,需要配置 `supportMethodsArguments` 参数为 `true`,同时要配置 `params` 参数。 例如下面的配置:

xml


在 MyBatis 方法中:

java List selectByPageNumSize( @Param(“user”) User user, @Param(“pageNumKey”) int pageNum, @Param(“pageSizeKey”) int pageSize);


当调用这个方法时,由于同时发现了 `pageNumKey` 和 `pageSizeKey` 参数,这个方法就会被分页。params 提供的几个参数都可以这样使用。

除了上面这种方式外,如果 User 对象中包含这两个参数值,也可以有下面的方法:

java List selectByPageNumSize(User user);


当从 User 中同时发现了 `pageNumKey` 和 `pageSizeKey` 参数,这个方法就会被分页。

注意:`pageNum` 和 `pageSize` 两个属性同时存在才会触发分页操作,在这个前提下,其他的分页参数才会生效。

#### 3). `PageHelper` 安全调用

##### 1. 使用 `RowBounds` 和 `PageRowBounds` 参数方式是极其安全的

##### 2. 使用参数方式是极其安全的

##### 3. 使用 ISelect 接口调用是极其安全的

ISelect 接口方式除了可以保证安全外,还特别实现了将查询转换为单纯的 count 查询方式,这个方法可以将任意的查询方法,变成一个 `select count(*)` 的查询方法。

##### 4. 什么时候会导致不安全的分页?

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

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

如果代码在进入 `Executor` 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 `MappedStatement` 时), 这种情况由于线程不可用,也不会导致 `ThreadLocal` 参数被错误的使用。

但是如果你写出下面这样的代码,就是不安全的用法:

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


这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

上面这个代码,应该写成下面这个样子:

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


这种写法就能保证安全。

如果你对此不放心,你可以手动清理 `ThreadLocal` 存储的分页参数,可以像下面这样使用:

java List list; if(param1 != null){ PageHelper.startPage(1, 10); try{ list = countryMapper.selectAll(); } finally { PageHelper.clearPage(); } } else { list = new ArrayList(); } ```

这么写很不好看,而且没有必要。