1). 使用 Maven
在 pom.xml 中添加如下依赖:
com.github.pagehelper
pagehelper
最新版本
2). 使用 Gradle
在 build.gradle
中添加:
dependencies {
compile("com.github.pagehelper:pagehelper:最新版本")
}
3). 使用 Spring Boot 时
Maven:
com.github.pagehelper
pagehelper-spring-boot-starter
最新版本
Gradle:
dependencies {
compile("com.github.pagehelper:pagehelper-spring-boot-starter:最新版本")
}
特别注意,新版拦截器是 com.github.pagehelper.PageInterceptor
。com.github.pagehelper.PageHelper
现在是一个特殊的 dialect
实现类,是分页插件的默认实现类,提供了和以前相同的用法。
1). 在 MyBatis 配置 xml 中配置拦截器插件
2). 在 Spring 配置文件中配置拦截器插件
使用 spring 的 XML 配置方式,可以使用 plugins
属性像下面这样配置:
params=value1
3). 在 Spring Boot 中配置
Spring Boot 引入 starter 后自动生效,对分页插件进行配置时,在 Spring Boot 对应的配置文件 application.[properties|yaml]
中配置:
properties:
pagehelper.propertyName=propertyValue
pagehelper.reasonable=false
pagehelper.defaultCount=true
yaml:
pagehelper:
propertyName: propertyValue
reasonable: false
defaultCount: true # 分页插件默认参数支持 default-count 形式,自定义扩展的参数,必须大小写一致
分页插件默认参数支持 default-count 形式,自定义扩展的参数,必须大小写一致。
支持的默认参数参考: PageHelperStandardProperties.java[1]
4). 分页插件横幅banner设置
为了避免多次配置分页插件导致的错误,配置分页插件后,启动时会输出 banner。
如果在项目启动时输出了多次 banner,就是配置了多次分页插件,根据日志输出的位置排查系统通过哪些方式配置了分页插件。
如果不想在启动时输出 banner,可以通过系统变量或环境变量关闭。
系统变量:-Dpagehelper.banner=false
环境变量:PAGEHELPER_BANNER=false
5). 分页插件参数介绍
分页插件提供了多个可选参数,这些参数使用时,按照上面配置方式中的示例配置即可。
分页插件可选参数如下:
debug
: 调试参数,默认 false
关闭,设置为 true
启用后,可以排查系统中存在的不安全调用,#查看如何安全调用[2]。 通过 PageHelper.startPage
等静态方法调用设置分页参数时,会记录当前执行的方法堆栈信息,当执行 MyBatis 的查询方法时,会使用设置好的分页参数, 此时会输出设置时的方法堆栈,通过查看堆栈,如果和当前执行的方法不一致,那么堆栈中对应的调用就是不安全调用,需要根据 #安全调用[3] 中的方式调整。输出的堆栈示例如下:
00:19:08.915 [main] DEBUG c.github.pagehelper.PageInterceptor - java.lang.Exception: 设置分页参数时的堆栈信息
at com.github.pagehelper.util.StackTraceUtil.current(StackTraceUtil.java:12)
at com.github.pagehelper.Page.
(Page.java:111) at com.github.pagehelper.Page.
(Page.java:126) at com.github.pagehelper.page.PageMethod.startPage(PageMethod.java:139)
at com.github.pagehelper.page.PageMethod.startPage(PageMethod.java:113)
at com.github.pagehelper.page.PageMethod.startPage(PageMethod.java:102)
at com.github.pagehelper.test.basic.PageHelperTest.testNamespaceWithStartPage(PageHelperTest.java:118)
...省略
00:19:09.069 [main] DEBUG c.g.pagehelper.mapper.UserMapper - Cache Hit Ratio [com.github.pagehelper.mapper.UserMapper]: 0.0
00:19:09.077 [main] DEBUG o.a.i.t.jdbc.JdbcTransaction - Opening JDBC Connection
00:19:09.078 [main] DEBUG o.a.i.t.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@6da21078]
00:19:09.087 [main] DEBUG c.g.p.m.UserMapper.selectAll_COUNT - ==> Preparing: SELECT count(1) FROM user
00:19:09.121 [main] DEBUG c.g.p.m.UserMapper.selectAll_COUNT - ==> Parameters:
00:19:09.131 [main] TRACE c.g.p.m.UserMapper.selectAll_COUNT - <== Columns: C1
00:19:09.131 [main] TRACE c.g.p.m.UserMapper.selectAll_COUNT - <== Row: 183
00:19:09.147 [main] DEBUG c.g.p.m.UserMapper.selectAll_COUNT - <== Total: 1
dialect
:默认情况下会使用 PageHelper 方式进行分页,如果想要实现自己的分页逻辑,可以实现 Dialect
(com.github.pagehelper.Dialect
) 接口,然后配置该属性为实现类的全限定名称。
countSuffix
:根据查询创建或者查找对应的 count 查询时,追加的 msId 后缀,默认 _COUNT
。
countMsIdGen
(5.3.2+):count 方法的 msId 生成方式,默认是 查询的 msId + countSuffix,想要自己定义时,可以实现 com.github.pagehelper.CountMsIdGen
接口,将该参数配置为实现的全限定类名即可。一个常见的用途: 在有Example查询的情况,selectByExample
可以使用对应的 selectCountByExample
方法进行 count 查询。
msCountCache
:自动创建查询的 count 查询方法时,创建的 count MappedStatement
会进行缓存,默认会优先查找 com.google.common.cache.Cache
的实现,如果项目没有 guava 依赖就会使用 mybatis 内置的 CacheBuilder
创建。想要对缓存进行细粒度的配置请参考源码: com.github.pagehelper.cache.CacheFactory
,两种默认方案提供了多个属性进行配置,也可以按照这里要求自己扩展实现。
下面几个参数都是针对默认 dialect 情况下的参数。使用自定义 dialect 实现时,下面的参数没有任何作用。
helperDialect
:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。 你可以配置helperDialect
属性来指定分页插件使用哪种方言。配置时,可以使用下面的缩写值:oracle
,mysql
,mariadb
,sqlite
,hsqldb
,postgresql
,db2
,sqlserver
,informix
,h2
,sqlserver2012
,derby
(完整内容看 PageAutoDialect[4])特别注意:使用 SqlServer2012 数据库时,需要手动指定为 sqlserver2012
,否则会使用 SqlServer2005 的方式进行分页,还可以设置 useSqlserver2012=true
将2012改为sqlserver的默认方式。 你也可以实现 AbstractHelperDialect
,然后配置该属性为实现类的全限定名称即可使用自定义的实现方法。
dialectAlias
:允许配置自定义实现的 别名,可以用于根据 JDBCURL 自动获取对应实现,允许通过此种方式覆盖已有的实现,配置示例如(多个时分号隔开):
当你使用的 jdbcurl 不在 PageAutoDialect[5]默认提供范围时,可以通过改参数实现自动识别。
useSqlserver2012
(sqlserver):使用 SqlServer2012 数据库时,需要手动指定为 sqlserver2012
,否则会使用 SqlServer2005 的方式进行分页,还可以设置 useSqlserver2012=true
将2012改为sqlserver的默认方式。
defaultCount
:用于控制默认不带 count 查询的方法中,是否执行 count 查询,默认 true
会执行 count 查询,这是一个全局生效的参数,多数据源时也是统一的行为。
countColumn
:用于配置自动 count 查询时的查询列,默认值0
,也就是 count(0)
,Page
对象也新增了 countColumn
参数,可以针对具体查询进行配置。
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
配置的字段中取值,查找到合适的值时就会自动分页。 使用方法可以参考测试代码中的 com.github.pagehelper.test.basic
包下的 ArgumentsMapTest
和 ArgumentsObjTest
。
autoRuntimeDialect
:默认值为 false
。设置为 true
时,允许在运行时根据多数据源自动识别对应方言的分页 (不支持自动选择sqlserver2012
,只能使用sqlserver
),用法和注意事项参考下面的场景五。
closeConn
:默认值为 true
。当使用运行时动态数据源或没有设置 helperDialect
属性自动获取数据库类型时,会自动获取一个数据库连接, 通过该属性来设置是否关闭获取的这个连接,默认true
关闭,设置为 false
后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定。
aggregateFunctions
(5.1.5+):默认为所有常见数据库的聚合函数,允许手动添加聚合函数(影响行数),所有以聚合函数开头的函数,在进行 count 转换时,会套一层。其他函数和列会被替换为 count(0) ,其中count列可以自己配置。
replaceSql
(sqlserver): 可选值为 regex
和 simple
,默认值空时采用 regex
方式,也可以自己实现 com.github.pagehelper.dialect.ReplaceSql
接口。
sqlCacheClass
(sqlserver): 针对 sqlserver 生成的 count 和 page sql 进行缓存,缓存使用的 com.github.pagehelper.cache.CacheFactory
,可选的参数和前面的 msCountCache
一样。
autoDialectClass
:增加 AutoDialect
接口用于自动获取数据库类型,可以通过 autoDialectClass
配置为自己的实现类,默认使用 DataSourceNegotiationAutoDialect
,优先根据连接池获取。 默认实现中,增加针对 hikari,druid,tomcat-jdbc,c3p0,dbcp
类型数据库连接池的特殊处理,直接从配置获取jdbcUrl,当使用其他类型数据源时,仍然使用旧的方式获取连接在读取jdbcUrl。 想要使用和旧版本完全相同方式时,可以配置 autoDialectClass=old
。当数据库连接池类型非常明确时,建议配置为具体值,例如使用 hikari 时,配置 autoDialectClass=hikari
,使用其他连接池时,配置为自己的实现类。
boundSqlInterceptors
:增加分页插件的 BoundSqlInterceptor
拦截器,可以在3个阶段对 SQL 进行处理或者简单读取, 增加参数 boundSqlInterceptors
,可以配置多个实现 BoundSqlInterceptor
接口的实现类名, 使用英文逗号隔开。PageHelper调用时,也可以通过类似PageHelper.startPage(x,x).boundSqlInterceptor(BoundSqlInterceptor boundSqlInterceptor)
针对本次分页进行设置。
keepOrderBy
:转换count查询时保留查询的 order by 排序。除全局配置外,可以针对单次操作进行设置。
keepSubSelectOrderBy
:转换count查询时保留子查询的 order by 排序。可以避免给所有子查询添加 /*keep orderby*/
,除全局配置外,可以针对单次操作进行设置。
sqlParser
:配置 JSqlParser 解析器,注意是 com.github.pagehelper.JSqlParser
接口,用于支持 sqlserver 等需要额外配置的情况。
重要提示:
当 offsetAsPageNum=false
的时候,由于 PageNum
问题,RowBounds
查询的时候 reasonable
会强制为 false
。使用 PageHelper.startPage
方法不受影响。
6. 如何选择配置这些参数
单独看每个参数的说明可能是一件让人不爽的事情,这里列举一些可能会用到某些参数的情况。
场景一
如果你仍然在用类似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
时,数据库连接只会获取一次,所以不需要担心占用的这一个连接是否会导致数据库出错,但是最好也根据数据源的特性选择是否关闭连接。
阅读前请注意看重要提示[6]
分页插件支持以下几种调用方式:
//第一种,RowBounds方式的调用
List
list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10)); //第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List
list = userMapper.selectIf(1); //第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List
list = userMapper.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 = userMapper.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 = userMapper.selectByPageNumSize(user); //第六种,ISelect 接口方式
//jdk6,7用法,创建接口
Page
page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() { @Override
public void doSelect() {
userMapper.selectGroupBy();
}
});
//jdk8 lambda用法
Page
page = PageHelper.startPage(1, 10).doSelectPage(()-> userMapper.selectGroupBy()); //也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
@Override
public void doSelect() {
userMapper.selectGroupBy();
}
});
//对应的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> userMapper.selectGroupBy());
//count查询,返回一个查询语句的count数
long total = PageHelper.count(new ISelect() {
@Override
public void doSelect() {
userMapper.selectLike(user);
}
});
//lambda
total=PageHelper.count(()->userMapper.selectLike(user));
下面对最常用的方式进行详细介绍
1). RowBounds方式的调用
List list=sqlSession.selectList("x.y.selectIf",null,new RowBounds(1,10));
使用这种调用方式时,你可以使用RowBounds参数进行分页,这种方式侵入性最小,我们可以看到,通过RowBounds方式调用只是使用了这个参数,并没有增加其他任何内容。
分页插件检测到使用了RowBounds参数时,就会对该查询进行物理分页。
关于这种方式的调用,有两个特殊的参数是针对 RowBounds
的,你可以参看上面的 场景一 和 场景二
注:不只有命名空间方式可以用RowBounds,使用接口的时候也可以增加RowBounds参数,例如:
//这种情况下也会进行物理分页查询
List
selectAll(RowBounds rowBounds);
注意: 由于默认情况下的 RowBounds
无法获取查询总数,分页插件提供了一个继承自 RowBounds
的 PageRowBounds
,这个对象中增加了 total
属性,执行分页查询后,可以从该属性得到查询总数。
2). PageHelper.startPage
静态方法调用
除了 PageHelper.startPage
方法外,还提供了类似用法的 PageHelper.offsetPage
方法。
在你需要进行分页的 MyBatis 查询方法前调用 PageHelper.startPage
静态方法即可,紧跟在这个方法后的第一个MyBatis 查询方法会被进行分页。
例一:
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
//紧跟着的第一个select方法会被分页
List
list = userMapper.selectIf(1); assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page
,如果想取出分页信息,需要强制转换为Page assertEquals(182, ((Page) list).getTotal());
例二:
//request: url?pageNum=1&pageSize=10
//支持 ServletRequest,Map,POJO 对象,需要配合 params 参数
PageHelper.startPage(request);
//紧跟着的第一个select方法会被分页
List
list = userMapper.selectIf(1); //后面的不会被分页,除非再次调用PageHelper.startPage
List
list2 = userMapper.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
的用法:
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
List
list = userMapper.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
参数。 例如下面的配置:
在 MyBatis 方法中:
List
selectByPageNumSize( @Param("user") User user,
@Param("pageNumKey") int pageNum,
@Param("pageSizeKey") int pageSize);
当调用这个方法时,由于同时发现了 pageNumKey
和 pageSizeKey
参数,这个方法就会被分页。params 提供的几个参数都可以这样使用。
除了上面这种方式外,如果 User 对象中包含这两个参数值,也可以有下面的方法:
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
参数被错误的使用。
但是如果你写出下面这样的代码,就是不安全的用法:
PageHelper.startPage(1, 10);
List
list; if(param1 != null){
list = userMapper.selectIf(param1);
} else {
list = new ArrayList
(); }
这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
上面这个代码,应该写成下面这个样子:
List
list; if(param1 != null){
PageHelper.startPage(1, 10);
list = userMapper.selectIf(param1);
} else {
list = new ArrayList
(); }
这种写法就能保证安全。
如果你对此不放心,你可以手动清理 ThreadLocal
存储的分页参数,可以像下面这样使用:
List
list; if(param1 != null){
PageHelper.startPage(1, 10);
try{
list = userMapper.selectAll();
} finally {
PageHelper.clearPage();
}
} else {
list = new ArrayList
(); }
这么写很不好看,而且没有必要。
如果和Spring集成不熟悉,可以参考下面两个
只有基础的配置信息,没有任何现成的功能,作为新手入门搭建框架的基础
集成 Spring 3.x[7]
集成 Spring 4.x[8]
这两个集成框架集成了 PageHelper 和 通用 Mapper[9]。
pagehelper-spring-boot-samples[10]
https://github.com/abel533/MyBatis-Spring-Boot
[1]
PageHelperStandardProperties.java: https://github.com/pagehelper/pagehelper-spring-boot/blob/master/pagehelper-spring-boot-autoconfigure/src/main/java/com/github/pagehelper/autoconfigure/PageHelperStandardProperties.java
[2]
#查看如何安全调用: #3-pagehelper-安全调用
[3]
#安全调用: #3-pagehelper-安全调用
[4]
PageAutoDialect: src/main/java/com/github/pagehelper/page/PageAutoDialect.java
[5]
PageAutoDialect: src/main/java/com/github/pagehelper/page/PageAutoDialect.java
[6]
重要提示: https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Important.md
[7]
集成 Spring 3.x: https://github.com/abel533/Mybatis-Spring/tree/spring3.x
[8]
集成 Spring 4.x: https://github.com/abel533/Mybatis-Spring
[9]
通用 Mapper: https://github.com/abel533/Mapper
[10]
pagehelper-spring-boot-samples: https://github.com/pagehelper/pagehelper-spring-boot/tree/master/pagehelper-spring-boot-samples