MyBatis 的插件应用:分页处理(分页插件 PageHelper)

MyBatis 框架虽然使用起来相当简单,且灵活,自己管理着 sql 语句,但是开发起来还是有着不少的工作量,比如在处理分页问题的时候,我们通常需要另外再查询一次总共的记录数

其实我们是希望把获取分页信息的工作给统一起来,简洁代码,减少工作量

我们可以利用 MyBatis 的插件功能(拦截器)来处理分页,这里我们尝试着自己去写一个简单的分页插件,后面也有介绍一个优秀的 MyBatis 分页插件 PageHelper 的使用方法

自定义一个简单的分页插件

在 mybatis xml 配置文件中,我们可以配置插件(plugins),MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用,这样我们可以通过拦截查询方法,添加分页查询条件,包装查询结果

创建一个插件

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可

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
/*
* 指定方法签名:
* 下面的配置表面 将会拦截在 Executor 实例中所有的 “query” 方法调用,
* 这里的 Executor 是负责执行低层映射语句的内部对象
*/
@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PageInterceptor implements Interceptor {

/**
* 拦截目标对象中目标方法的执行
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
//TODO 覆盖目标方法,我们在这里覆盖目标方法

// 执行目标方法,并返回结果(这里没有处理)
return invocation.proceed();
}

/**
* 包装目标对象,即为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object target) {
// 借助 Plugin 的 wrap(Object target,Interceptor interceptor); 包装我们的目标对象
// target: 目标对象, interceptor: 拦截器, this 表示使用当前拦截器
return Plugin.wrap(target, this);
}

/**
* 获取插件注册时,传入的property属性
*/
@Override
public void setProperties(Properties properties) {
// TODO
}
}

同时在 MyBatis 的配置文件中注册插件

1
2
3
4
<plugins> 
<plugin interceptor="com.brave.interceptor.PageInterceptor">
</plugin>
</plugins>

上面的只是先创建了一个拦截器,还没有做具体的处理,现在我们先理清楚需要做的事情

  1. 拦截查询的目标方法,通过 Invocation 参数获取目标方法原来的参数信息
  2. 因为是分页查询,需要传入分页的参数条件,将通过是否传入有效的分页参数来判断是否要进行分页处理
  3. 除了要执行原本的查询语句,还要查询count总记录数,并计算出其他分页信息
  4. 将查询出来的结果和分页信息包装在一起并返回结果

创建接口和返回结果集

查询的时候,我们需要传入分页条件,这里我们创建一个 IPage 接口,为了规范统一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface IPage {

/**
* 当前页
*/
Integer getPageNum();

/**
* 每页数量
*/
Integer getPageSize();

/**
* 开始行
*/
Integer getStartRow();

/**
* 排序条件
*/
String getOrderBy();
}

并实现一个简单类 PageConfig.java

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
public class PageConfig implements IPage {
private Integer pageNum;
private Integer pageSize;
private Integer startRow;
private String orderBy;

public PageConfig() {
this(1,10);
}
public PageConfig(Integer pageNum,Integer pageSize) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.startRow = (this.pageNum -1) * this.pageSize;
}
public Integer getPageNum() {
return pageNum;
}
public void setPageNum(Integer pageNum) {
this.pageNum = pageNum;
this.startRow = (this.pageNum -1) * this.pageSize;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
this.startRow = (this.pageNum -1) * this.pageSize;
}
public String getOrderBy() {
return orderBy;
}
public void setOrderBy(String orderBy) {
this.orderBy = orderBy;
}
public Integer getStartRow() {
return startRow;
}
public void setStartRow(Integer startRow) {
this.startRow = startRow;
}
}

同时我们需要包装一下返回结果集,MyBatis 直接查询返回的结果集类型为 ArrayList,我们要在此基础上加上分页信息

Page.java

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
public class Page<E> extends ArrayList<E> {
private static final long serialVersionUID = 1L;

private int pageNum; // 页码
private int pageSize; // 每页数量
private long total; // 总记录数
private int pages; // 页数

public Page() {
super();
}
public Page(int pageNum, int pageSize) {
this(pageNum, pageSize, 0);
}
public Page(int pageNum, int pageSize, long total) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.total = total;
this.pages = (int)(this.pageSize == 0 ? 0
: this.total % this.pageSize == 0 ? this.total / this.pageSize : (this.total / this.pageSize + 1));
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public Long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
this.pages = (int)(this.pageSize == 0 ? 0
: this.total % this.pageSize == 0 ? this.total / this.pageSize : (this.total / this.pageSize + 1));
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}

@Override
public String toString() {
return "Page [pageNum=" + pageNum + ", pageSize=" + pageSize + ", total=" + total + ", pages=" + pages + "]";
}
}

处理查询方法

我们再顺一下拦截查询方法之后的流程

  1. 通过 Invocation 参数获取目标方法原来的参数信息
  2. 分析参数是否要进行分页处理,不需要不处理,执行原来的方法
  3. 添加分页条件,并查询结果(包括查询总数)
  4. 包装结果集并返回

这里直接附上实现代码

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/*
* 指定方法签名: 下面的配置表面 将会拦截在 Executor 实例中的 “query” 方法调用, 这里的 Executor 是负责执行低层映射语句的内部对象
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Intercepts({ @Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PageInterceptor implements Interceptor {
private final static String COUNT_SUFFIX = "_COUNT"; // count查询语句Id后缀

/**
* 拦截目标对象中目标方法的执行
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获得目标对象
Executor executor = (Executor)invocation.getTarget();
// 获得目标方法的参数
Object[] args = invocation.getArgs();
// MappedStatement表示的是XML中的一个SQL
MappedStatement mappedStatement = (MappedStatement)args[0];
Object parameter = args[1];
System.out.println(parameter.getClass());
RowBounds rowBounds = (RowBounds)args[2];
ResultHandler resultHandler = (ResultHandler)args[3];

BoundSql boundSql = mappedStatement.getBoundSql(parameter);
CacheKey cacheKey = executor.createCacheKey(mappedStatement, parameter, rowBounds, boundSql);

//获取分页参数
IPage pageConfig = getPageConfig(parameter);
if (pageConfig != null) {
return pageQuery(executor, mappedStatement, parameter, resultHandler, boundSql, cacheKey,
pageConfig);
} else {
// 不用分页,执行目标方法
return invocation.proceed();
}

}

/**
* 分页查询
*
* @param executor
* @param ms
* @param parameter
* @param resultHandler
* @param boundSql
* @param cacheKey
* @param pageConfig
* @return
* @throws SQLException
*/
private Object pageQuery(Executor executor, MappedStatement ms, Object parameter,
ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey, IPage pageConfig) throws SQLException {
Page page = new Page(pageConfig.getPageNum(), pageConfig.getPageSize());

// 查询总数
Long count = countQuery(executor, ms, parameter, resultHandler, boundSql);
// 总数大于 0 ,开始分页查询
if (count != null && count > 0) {
page.setTotal(count);
// 生成分页的缓存 key
CacheKey pageKey = cacheKey;
// 获取分页 sql
StringBuilder pageSql = new StringBuilder(boundSql.getSql());
if (pageConfig.getStartRow() == 0) {
pageSql.append(" LIMIT ").append(pageConfig.getPageSize());
} else {
pageSql.append(" LIMIT ").append(pageConfig.getStartRow()).append(",").append(pageConfig.getPageSize());
}
BoundSql pageBoundSql =
new BoundSql(ms.getConfiguration(), pageSql.toString(), boundSql.getParameterMappings(), parameter);

List list = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
page.addAll(list);
}
return page;
}

/**
* count 查询,获取总记录数
*
* @param executor
* @param mappedStatement
* @param parameter
* @param resultHandler
* @param boundSql
* @return
* @throws SQLException
*/
private Long countQuery(Executor executor, MappedStatement mappedStatement, Object parameter,
ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 创建新的 MappedStatement 用于count查询
MappedStatement countMs = newCountMappedStatement(mappedStatement);
// 创建 count 查询的缓存 key
CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
// 创建 countBoundSql
String countSql = "SELECT COUNT(*) FROM (" + boundSql.getSql() + ") T";
BoundSql countBoundSql =
new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
// 执行 count 查询
Object countResult =
executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
Long count = (Long)((List)countResult).get(0);
return count;
}

/**
* 新建用于count查询的MappedStatement
*
* @param ms
* @return
*/
private MappedStatement newCountMappedStatement(MappedStatement ms) {
String countMsId = ms.getId() + COUNT_SUFFIX;
MappedStatement.Builder builder =
new MappedStatement.Builder(ms.getConfiguration(), countMsId, ms.getSqlSource(), ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());

if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
StringBuilder keyProperties = new StringBuilder();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
}
// 设置返回结果为 Long
List<ResultMap> resultMaps = new ArrayList<ResultMap>();
ResultMap resultMap =
new ResultMap.Builder(ms.getConfiguration(), ms.getId(), Long.class, new ArrayList<ResultMapping>(0))
.build();
resultMaps.add(resultMap);
builder.resultMaps(resultMaps);

return builder.build();
}

/**
* 获取分页信息
*
* @param parameter
* @param rowBounds
* @return
*/
private IPage getPageConfig(Object parameter) {
IPage pageConfig = null;
if (parameter instanceof IPage) {
pageConfig = (IPage)parameter;
}else if(parameter instanceof MapperMethod.ParamMap) {
MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap)parameter;
for(Object value:paramMap.values()) {
if (value instanceof IPage) {
pageConfig = (IPage)value;
break;
}
}
}
return pageConfig;
}

/**
* 包装目标对象,即为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object target) {
// 借助 Plugin 的 wrap(Object target,Interceptor interceptor); 包装我们的目标对象
// target: 目标对象, interceptor: 拦截器, this 表示使用当前拦截器
return Plugin.wrap(target, this);
}

/**
* 获取插件注册时,传入的property属性
*/
@Override
public void setProperties(Properties properties) {

}
}

测试结果

要使用插件,记得在 mybatis xml 中配置 plugin

1
2
3
4
<plugins>
<plugin interceptor="com.brave.interceptor.PageInterceptor">
</plugin>
</plugins>

我这个例子适用于使用 mapper 接口,只要接口方法有 IPage 的分页查询参数,就会分页查询

1
2
3
4
5
public interface UpmsUserMapper {
List<UpmsUser> selectUser(UpmsUser upmsUser, PageConfig pageConfig);

List<UpmsUser> selectUser(IPage pageConfig);
}

我们创建测试类测试一下结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(SpringJUnit4ClassRunner.class)  //使用junit4进行测试  
@ContextConfiguration ("/conf/spring/applicationContext*.xml")
public class UpmsUserMapperTest {

@Autowired
private UpmsUserMapper upmsUserMapper;

@Test
public void testSelectUserUpmsUserPageConfig() {
PageConfig pageConfig = new PageConfig(1, 2);
List<UpmsUser> list = upmsUserMapper.selectUser(new UpmsUser(), pageConfig);
System.out.println(list.getClass());
System.out.println(list.toString());
for (UpmsUser upmsUser : list) {
System.out.println(upmsUser.toString());
}
}
}

测试结果如下

1
2
3
4
class com.brave.page.Page
Page [pageNum=1, pageSize=2, total=3, pages=2]
[userId=10001][loginname=zou][password=123456][locked=null]
[userId=10002][loginname=zou][password=123456][locked=null]

分页插件 PageHelper 使用

上面的例子,只是简陋的实现一个分页插件,明白大概的流程。下面来介绍分页插件 PageHelper 的使用

PageHelper 的 github 地址 https://github.com/pagehelper/Mybatis-PageHelper

添加依赖

1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.8</version>
</dependency>

由于使用了 sql 解析工具,还会引入 jsqlparser.jar 包,如果不是使用 maven,还需要自己导入 pagehelper 依赖版本一致的 jsqlparser.jar 包

配置拦截器插件

在 MyBatis 配置文件中添加拦截器插件配置

1
2
3
4
5
6
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 具体参数后面介绍 -->
<property name="helperDialect" value="mysql"/>
</plugin>
</plugins>

如果是 Spring 项目,也可以在 Spring 配置文件的 SqlSessionFactoryBean 里面添加插件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!-- 配置映射器文件 -->
<property name="mapperLocations" value="classpath*:com/brave/dao/mapper/*Mapper.xml" />

<!-- 插件 -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<!-- 具体参数后面介绍 -->
<value>
helperDialect=mysql
</value>
</property>
</bean>
</array>
</property>
</bean>

两个地方的配置选其一就好,不要同时生效

下面是具体分页插件参数

参数 描述 默认值
helperDialect 分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。该属性可以指定分页插件使用哪种方言。配置值:oracle,mysql,mariadb,sqlite,hsqldb,postgresql,db2,sqlserver,informix,h2,sqlserver2012,derby
offsetAsPageNum 该参数对使用 RowBounds 作为分页参数时有效。设为 true 时,会将 RowBounds 中的 offset 参数当成 pageNum 使用,可以用页码和页面大小两个参数进行分页 false
rowBoundsWithCount 该参数对使用 RowBounds 作为分页参数时有效。设置为true时,使用 RowBounds 分页会进行 count 查询 false
pageSizeZero 设置为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型) false
reasonable 分页合理化参数。设置为 true 时,pageNum<=0 时会查询第一页, pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询 false
params 为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值,可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
supportMethodsArguments 支持通过 Mapper 接口参数来传递分页参数,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。 使用方法可以参考测试代码中的 com.github.pagehelper.test.basic 包下的 ArgumentsMapTest 和 ArgumentsObjTest false
autoRuntimeDialect 设置为 true 时,允许在运行时根据多数据源自动识别对应方言的分页 false
closeConn 当使用运行时动态数据源或没有设置 helperDialect 属性自动获取数据库类型时,会自动获取一个数据库连接,通过该属性来设置是否关闭获取的这个连接,默认true关闭,设置为 false 后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定 true
aggregateFunctions(5.1.5+) 默认为所有常见数据库的聚合函数,允许手动添加聚合函数(影响行数),所有以聚合函数开头的函数,在进行 count 转换时,会套一层。其他函数和列会被替换为 count(0),其中count列可以自己配置。

当 offsetAsPageNum=false 的时候,由于 PageNum 问题,RowBounds查询的时候 reasonable 会强制为 false。使用 PageHelper.startPage 方法不受影响。

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

使用方法

这里主要是4种使用方法

  • RowBounds方式的调用
  • PageHelper 的静态方法调用
  • 接口方法使用分页参数
  • ISelect 接口方式

RowBounds方式的调用

使用 RowBounds 参数进行分页,这种方式侵入性最小,用只是使用了这个参数,并没有增加其他任何内容

1
2
3
4
5
6
7
8
9
@Repository
public class UpmsUserDaoImpl extends SuperDao implements UpmsUserDao {

@Override
public List<UpmsUser> selectUser(int offset,int limit) {
List<UpmsUser> list = getSqlSession().selectList("com.brave.dao.UpmsUserDao2.selectUser", null, new RowBounds(offset, limit));
return list;
}
}

测试代码

1
2
3
4
5
6
7
@Test
public void testSelectUser() {
List<UpmsUser> upmsUsers = upmsUserDao.selectUser(0, 10);
for (UpmsUser upmsUser : upmsUsers) {
System.out.println(upmsUser.toString());
}
}

我们在配置插件的时候有两个参数是关于 RowBounds 的

  • offsetAsPageNum 设置为 true 时,会将 RowBounds 中的 offset 参数当成 pageNum 使用,可以用页码和页面大小两个参数进行分页,也就是说上面的例子,我们查第一页时为 upmsUserDao.selectUser(1, 10)
  • rowBoundsWithCount RowBounds 查询默认是没有 count 查询的,设置为true时,使用 RowBounds 分页会进行 count 查询

当 rowBoundsWithCount 设置为true时,会有 count 查询,会获得查询记录总数

1
2
3
4
5
6
7
8
@Test
public void testSelectUser() {
Page<UpmsUser> upmsUsers = (Page<UpmsUser>)upmsUserDao.selectUser(1, 10);
System.out.println("总页数:"+upmsUsers.getTotal());
for (UpmsUser upmsUser : upmsUsers) {
System.out.println(upmsUser.toString());
}
}

没有设置或为false的话,total 为 -1

如果不设置 rowBoundsWithCount,也可以通过 PageRowBounds 进行 count 查询

getSqlSession().selectList("com.brave.dao.UpmsUserDao.selectUser", null, new PageRowBounds (offset, limit))

PageRowBounds 继承 RowBounds,多了 total 属性

另外使用接口的时候也可以增加RowBounds参数

List<Country> selectAll(RowBounds rowBounds);

PageHelper 的静态方法调用

PageHelper 类有 startPage 和 offsetPage 方法去设置分页参数

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

通常是在 service 的 bean 中调用 dao 的查询方法前 或者是 mapper 接口方法前 调用 PageHelper 的静态方法

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testSelectUser2() {
// request: url?pageNum=1&pageSize=10
// 支持 ServletRequest,Map,POJO 对象,需要配合 params 参数
// PageHelper.startPage(request);
PageHelper.startPage(1, 10);
Page<UpmsUser> upmsUsers = (Page<UpmsUser>)upmsUserDao.selectUser();
System.out.println("总页数:" + upmsUsers.getTotal());
for (UpmsUser upmsUser : upmsUsers) {
System.out.println(upmsUser.toString());
}
}

使用 mapper 接口的时候

1
2
3
4
5
6
7
8
9
10
11
@Service
public class UpmsUserServiceImpl implements UpmsUserService {
@Autowired
private UpmsUserMapper upmsUserMapper;

@Override
public List<UpmsUser> listUser(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
return upmsUserMapper.selectUser();
}
}

使用 PageInfo

PageInfo 包含了非常全面的分页属性

1
2
List<UpmsUser> list = (Page<UpmsUser>)upmsUserDao.selectUser();
PageInfo<UpmsUser> pageInfo = new PageInfo(list);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private int pageNum;//当前页
private int pageSize;//每页的数量
private int size; //当前页的数量

//由于startRow和endRow不常用,这里说个具体的用法
//可以在页面中"显示startRow到endRow 共size条数据"
private int startRow;//当前页面第一个元素在数据库中的行号
private int endRow;//当前页面最后一个元素在数据库中的行号

private int pages;//总页数
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;//导航条上的最后一页

使用参数的方法

想要使用参数方式,需要配置 supportMethodsArguments 参数为 true,同时要配置 params 参数

1
2
3
4
5
6
7
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="supportMethodsArguments" value="true"/>
<!-- params 默认值为 pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero -->
<property name="params" value="pageNum=pageNum;pageSize=pageSize;"/>
</plugin>
</plugins>

可以在使用接口声明时就带上 pageNum 和 pageSize

1
2
3
4
5
6
7
8
9
10
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(
@Param("upmsUser") UpmsUser upmsUser,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<Country> list = countryMapper.selectByPageNumSize(user, 1, 10);

也可以把 pageNum 和 pageSize 存在于对象中,如 UpmsUser

1
2
3
4
5
6
//存在以下 Mapper 接口方法,不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(UpmsUser upmsUser);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<Country> list = countryMapper.selectByPageNumSize(upmsUser);

两种情况都需要 pageNum 和 pageSize 两个属性同时存在才会分页,且在 xml 中的sql中不需要处理这两个参数

ISelect 接口方式

另外还可以使用 ISelect 接口方式方式

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
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
@Override
public void doSelect() {
upmsUserMapper.selectUser();
}
});
// lambda用法
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(()-> upmsUserMapper.selectUser());

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

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

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

上述使用方法的安全性考虑

  • 使用 RowBounds 和 PageRowBounds 参数方式是极其安全的
  • 使用参数方式是极其安全的
  • 使用 ISelect 接口调用是极其安全的

不安全的时候:

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

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

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

下面这样的代码,就是不安全的用法:

1
2
3
4
5
6
7
8
//由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页
PageHelper.startPage(1, 10);
List<UpmsUser> list;
if(param1 != null){
list = UpmsUserMapper.selectUser(param1);
} else {
list = new ArrayList<UpmsUser>();
}

我们只要把 PageHelper.startPage(1, 10); 方法放到if里面查询语句之前就好了