分页插件-PageHelper
介绍
- PageHelper是国内非常优秀的一款开源的mybatis分页插件,它支持基本主流与常用的数据库,例如mysql、 oracle、mariaDB、DB2、SQLite、Hsqldb等。
- 项目在 github 的项目地址:https://github.com/pagehelper/Mybatis-PageHelper
- 在gitee的地址:https://gitee.com/free/Mybatis_PageHelper
- API文档 https://apidoc.gitee.com/free/Mybatis_PageHelper/
- 使用文档: https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md
集成方式
- 两种集成的方式
引jar包的方式
你可以从下面的地址中下载最新版本的 jar 包:
由于使用了sql 解析工具,你还需要下载 jsqlparser.jar:
使用Maven(主要)
pom.xml中引入依赖:
1
2
3
4
5<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>最新版本</version>
</dependency>
配置说明
特别注意,新版拦截器是
com.github.pagehelper.PageInterceptor
。com.github.pagehelper.PageHelper
是一个特殊的dialect
(方言)实现类,是分页插件的默认实现类,提供了和以前相同的用法。下面二种方式选一个即可
MyBatis配置拦截器插件
配置如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14<!--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>
Spring配置拦截器插件
使用 spring 的属性配置方式,可以使用 plugins 属性像下面这样配置:
1
2
3
4
5
6
7
8
9
10
11
12
13<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注意其他配置 -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<!--使用下面的方式配置参数,一行配置一个 -->
<value>params=value1</value>
</property>
</bean>
</array>
</property>
</bean>
配置拦截器参数
想要使用参数方式,需要配置
supportMethodsArguments
参数为true
,同时要配置params
参数。 例如下面的配置:1
2
3
4
5
6
7
8<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
<property name="supportMethodsArguments" value="true"/>
<property name="params" value="pageNum=pageNumKey;pageSize=pageSizeKey;"/>
</plugin>
</plugins>在 MyBatis 方法中:
1
2
3
4List<User> selectByPageNumSize(
User user,
int pageNum,
int pageSize);当调用这个方法时,由于同时发现了
pageNumKey
和pageSizeKey
参数,这个方法就会被分页。params 提供的几个参数都可以这样使用。除了上面这种方式外,如果 User 对象中包含这两个参数值,也可以有下面的方法:
- 当从 User 中同时发现了
pageNumKey
和pageSizeKey
参数,这个方法就会被分页。 - 注意:
pageNum
和pageSize
两个属性同时存在才会触发分页操作,在这个前提下,其他的分页参数才会生效。
1
List<User> selectByPageNumSize(User user);
- 当从 User 中同时发现了
Spring Boot 集成
添加依赖
添加pom依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<!-- MyBatis 分页插件 PageHelper -->
<pagehelper.version>5.2.0</pagehelper.version>
<pagehelper-springboot.version>1.3.0</pagehelper-springboot.version>
<!-- PageHelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper-springboot.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-autoconfigure</artifactId>
<version>${pagehelper-springboot.version}</version>
</dependency>
依赖说明
基本依赖如下
1
2
3
4
5
6
7
8
9
10
11
12<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.4</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>这样的话就可以用直接使用分页了
1
2
3PageHelper.startPage(page,limit);
List<HashMap<String, Object>> list = xxxBiz.selectAll();
PageInfo<HashMap<String, Object>> pageInfo = new PageInfo<>(list);想要以下一下扩展功能的话需要添加依赖
依赖如下
1
2
3
4
5
6<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-autoconfigure</artifactId>
<version>1.2.5</version>
</dependency>扩展功能如下
- helper-dialect:指定数据库,不指定的话会默认自动检测数据库类型
- reasonable:是否启用分页合理化。
- 如果启用,当pagenum<1时,会自动查询第一页的数据,
- 当pagenum>pages时,自动查询最后一页数据;
- 不启用的,以上两种情况都会返回空数据
- support-methods-arguments:默认值false,分页插件会通过Mapper接口参数来传递分页参数,方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。
- params:用于从对象中根据属性名取值, 可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,
- 不配置映射的用默认值, 默认值为
pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
- 不配置映射的用默认值, 默认值为
1
2
3
4
5
6pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
# Map中发现了countSql属性,就会作为count参数使用。
params: count=countSql
分页插件介绍
配置参数
helperDialect:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。
- 你可以配置helperDialect属性来指定分页插件使用哪种方言。
- 配置时,可以使用下面的缩写值:oracle , mysql , mariadb , sqlite , hsqldb , postgresql , db2 , sqlserver , informix , h2 , sqlserver2012 , derby
- 特别注意:使用SqlServer2012数据库时,需要手动指定为sqlserver2012,否则会使用SqlServer2005的方式进行分页。
- 你也可以实现(AbstractHelperDialect,然后配置该属性为实现类的全限定名称即可使用自定义的实现方法。
offsetAsPageNum:默认值为false,该参数对使用RowBounds作为分页参数时有效。
- 当该参数设置为true时,会将[RowBounds中的offset参数当成pageNum使用,可以用页码和页面大小两个参数进行分页。
rowBoundswithCount :默认值为false,该参数对使用RowBounds 作为分页参数时有效。
- 当该参数设置为true时,使用RowBounds分页会进行count查询。
pageSizeZero:默认值为false,
- 当该参数设置为true时,如果
pageSize=0
或者RowBounds.limit =0
就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是Page类型)。
- 当该参数设置为true时,如果
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
。
supportNethodsArguments :支持通过Mapper接口参数来传递分页参数,默认值false,
- 分页插件会从查询方法的参数值中,自动根据上面params配置的字段中取值,查找到合适的值时就会自动分页。
- 使用方法可以参考测试代码中的com.github.pagehelper.test.basic包下的ArgumentsMapTest和ArgumentsobjTest 。
autoRuntimeDialect:默认值为false。
- 设置为true时,允许在运行时根据多数据源自动识别对应方言的分页(不支持自动选择sqlserver2012,只能使用sqlserver),用法和注意事项参考下面的场景五。
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
时,数据库连接只会获取一次,所以不需要担心占用的这一个连接是否会导致数据库出错,但是最好也根据数据源的特性选择是否关闭连接。
PageInfo属性
属性如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48public class PageInfo<T> implements Serializable {
private static final long serialVersionUID = 1L;
//当前页
private int pageNum;
//每页的数量
private int pageSize;
//当前页的数量
private int size;
//由于startRow和endRow不常用,这里说个具体的用法
//可以在页面中"显示startRow到endRow 共size条数据"
//当前页面第一个元素在数据库中的行号
private int startRow;
//当前页面最后一个元素在数据库中的行号
private int endRow;
//总记录数
private long total;
//总页数
private int pages;
//结果集
private List<T> list;
//前一页
private int prePage;
//下一页
private int nextPage;
//是否为第一页
private boolean isFirstPage = false;
//是否为最后一页
private boolean isLastPage = false;
//是否有前一页
private boolean hasPreviousPage = false;
//是否有下一页
private boolean hasNextPage = false;
//导航页码数
private int navigatePages;
//所有导航页号
private int[] navigatepageNums;
//导航条上的第一页
private int navigateFirstPage;
//导航条上的最后一页
private int navigateLastPage;
...各类方法
}
基本使用
PageHelper的基本使用有6种,大家可以查看文档,最常用的有两种
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69//第一种,RowBounds方式的调用
List<User> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));
//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectIf(1);
//第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<User> list = userMapper.selectIf(1);
//第四种,参数方法调用
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<User> selectByPageNumSize(
User user,
int pageNum,
int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<User> 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<User> selectByPageNumSize(User user);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<User> list = userMapper.selectByPageNumSize(user);
//第六种,ISelect 接口方式
//jdk6,7用法,创建接口
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
public void doSelect() {
userMapper.selectGroupBy();
}
});
//jdk8 lambda用法
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(()-> userMapper.selectGroupBy());
//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
public void doSelect() {
userMapper.selectGroupBy();
}
});
//对应的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> userMapper.selectGroupBy());
//count查询,返回一个查询语句的count数
long total = PageHelper.count(new ISelect() {
public void doSelect() {
userMapper.selectLike(user);
}
});
//lambda
total = PageHelper.count(()->userMapper.selectLike(user));
RowBounds方式(了解)
使用这种调用方式时,你可以使用RowBounds参数进行分页,这种方式侵入性最小,我们可以看到,通过 RowBounds方式调用只是使用了这个参数,并没有增加其他任何内容。
1
List<User> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));
分页插件检测到使用了RowBounds参数时,就会对该查询进行物理分页, 查询结果,就是我们分页后的结果,性能是最好
关于这种方式的调用,有两个特殊的参数是针对
RowBounds
的,你可以参看上面的分页插件参数介绍注意:
认情况下的 RowBounds 无法获取查询总数,分页插件提供了一个继承自 RowBounds 的 PageRowBounds ,这个对象中增加了 total 属性,执行分页查询后,可以从该属性得到查询总数。
不只有命名空间方式可以用RowBounds,使用接口的时候也可以增加RowBounds参数,例如:
1
2//这种情况下也会进行物理分页查询
List<Country> selectAll(RowBounds rowBounds);
静态方法调用(重点)
在你需要进行分页的 MyBatis 查询方法前调用
PageHelper.startPage 静态方法
即可,紧跟在这个方法后的第一个MyBatis 查询方法
会被进行分页。例一:
1
2
3
4
5
6
7
8//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
//紧跟着的第一个select方法会被分页
List<User> list = userMapper.selectIf(1);
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page<E>,如果想取出分页信息,需要强制转换为Page<E>
assertEquals(182, ((Page) list).getTotal());例二:请求参数带分页信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//request: url?pageNum=1&pageSize=10
//支持 ServletRequest,Map,POJO 对象,需要配合 params 参数
PageHelper.startPage(request);
//紧跟着的第一个select方法会被分页
List<User> list = userMapper.selectIf(1);
//后面的不会被分页,除非再次调用PageHelper.startPage
List<User> list2 = userMapper.selectIf(null);
//list1
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page<E>,如果想取出分页信息,需要强制转换为Page<E>,
//或者使用PageInfo类(下面的例子有介绍)
assertEquals(182, ((Page) list).getTotal());
//list2
assertEquals(1, list2.get(0).getId());
assertEquals(182, list2.size());例三:
PageInfo
使用:实际返回的结果list类型是Page<E>
,可以通过构造方法创建PageInfo对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
List<User> 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());
PageHelper
调用安全
极其安全
- 使用
RowBounds
和PageRowBounds
参数方式是极其安全的 - 使用参数方式是极其安全的
- 使用 ISelect 接口调用是极其安全的
- 使用
ISelect 接口方式除了可以保证安全外,还特别实现了将查询转换为单纯的 count 查询方式,这个方法可以将任意的查询方法,变成一个
select count(*)
的查询方法。不安全的分页
PageHelper
方法使用了静态的ThreadLocal
参数,分页参数和线程是绑定的。- 只要你可以保证在
PageHelper
方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为PageHelper
在finally
代码段中自动清除了ThreadLocal
存储的对象。 - 如果代码在进入
Executor
前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到MappedStatement
时), 这种情况由于线程不可用,也不会导致ThreadLocal
参数被错误的使用。
但是如果你写出下面这样的代码,就是不安全的用法:
这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费
这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
1
2
3
4
5
6
7PageHelper.startPage(1, 10);
List<User> list;
if(param1 != null){
list = userMapper.selectIf(param1);
} else {
list = new ArrayList<User>();
}上面这个代码,应该写成下面这个样子:这种写法就能保证安全。
1
2
3
4
5
6
7List<User> list;
if(param1 != null){
PageHelper.startPage(1, 10);
list = userMapper.selectIf(param1);
} else {
list = new ArrayList<User>();
}如果你对此不放心,你可以手动清理
ThreadLocal
存储的分页参数,可以像下面这样使用:这么写很不好看,而且没有必要。1
2
3
4
5
6
7
8
9
10
11List<User> list;
if(param1 != null){
PageHelper.startPage(1, 10);
try{
list = userMapper.selectAll();
} finally {
PageHelper.clearPage();
}
} else {
list = new ArrayList<User>();
}
整合案例
pom.xml
添加如下依赖1
2
3
4
5
6<!-- pagehelper分页插件依赖 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>配置文件
application.yml
中添加:1
2
3
4
5pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countsql编写Controller
1
2
3
4
5
6
7
8
public String getAllPerson(Model model,{ Integer pageNum)
PageHelper.startPage(pageNum,5);
List<Person> list = personService.getAllPerson();
PageInfo<Person> pageInfo = new PageInfo<Person>(list);
model.addAttribute("pageInfo",pageInfo);
return "list";
}其中:
PageHelper.startPage(int PageNum,int PageSize)
:用来设置页面的位置和展示的数据条目数,我们设置每页展示5条数据。PageInfo用来封装页面信息,返回给前台界面。PageInfo中的一些我们需要用到的参数如下表:
PageInfo.list 结果集 PageInfo.pageNum 当前页码 PageInfo.pageSize 当前页面显示的数据条目 PageInfo.pages 总页数 PageInfo.total 数据的总条目数 PageInfo.prePage 上一页 PageInfo.nextPage 下一页 PageInfo.isFirstPage 是否为第一页 PageInfo.isLastPage 是否为最后一页 PageInfo.hasPreviousPage 是否有上一页 PageHelper.hasNextPage 是否有下一页
注意事项
PageHelper.startPage
方法只有紧跟在PageHelper.startPage
方法后的第一个Mybatis的查询(Select)方法会被分页。请不要在系统中配置多个分页插件
- 使用Spring时,
mybatis-config.xml
和Spring<bean>
配置方式,请选择其中一种,不要同时配置多个分页插件!
- 使用Spring时,
分页插件不支持带有
for update
语句的分页- 对于带有
for update
的sql,会抛出运行时异常,对于这样的sql建议手动分页,毕竟这样的sql需要重视。
- 对于带有
分页插件不支持嵌套结果映射
- 由于嵌套结果方式会导致结果集被折叠,因此分页查询的结果在折叠后总数会减少,所以无法保证分页结果数量正确。