查看pagehelper/Mybatis-PageHelper项目的github,先了解下最新版本
点击maven center可以跳转search.maven.org
找到右侧maven,复制依赖
pom 引入Pagehelper分页插件
com.github.pagehelper
pagehelper
5.2.0
这里先跳过如何一步一步的实现流程,我们先来看看上面的依赖带来的问题。
从下面的图片可以看到,查询接口的时候,返回的分页数据明显是不对。总共3条数据,分页第十页,前面跳过90条数据,那么必然是返回空数据的,结果明显是有问题的。
github上面的依赖用在springboot上可能有点问题,我们需要使用专门的springboot的版本pagehelper-spring-boot-starter插件
在search.maven.org查询pagehelper-spring-boot-starter
从上面查询到最新的版本是1.3.0,pom.xml添加依赖如下:
com.github.pagehelper
pagehelper-spring-boot-starter
1.3.0
无数据的时候返回空
注意上面的分析是基于application.yml中pageHelper的reasonable设置为false的前提下的
reasonable为true,如果存在数据,但是该分页没有数据的时候会返回最后一条数据
配置分页插件
application.yml添加如下几行配置信息
#pageHelper分页插件
pageHelper:
helperDialect: mysql
reasonable: false #如果开启分页优化,在分页的时候如果没有数据,会显示有数据页面的数据
supportMethodsArguments: true
params: count=countSql
使用pageHelper实现分页功能
1.创建UserEntity实体类
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(value = {"password"})
public class UserEntity {
/** 会员级别ID;1->黄金会员;2->白金会员;3->钻石会员;4->普通会员 */
public int memberLevelId ;
/** 用户名 */
public String username ;
/** 密码 */
public String password ;
/** 手机号码 */
public String phone ;
/** 昵称 */
public String nickname ;
/** 帐号启用状态;0->禁用;1->启用 */
public int status ;
/** 注册时间 */
public String createTime ;
}
2.数据操作层Mapper类查询出所有数据
@Repository
public interface UserMapper {
List getUserList();
}
UserMapper.xml
3、创建业务接口
@Repository
public class UserService {
@Autowired
private UserMapper mUserMapper;
public List getUserList() {
return mUserMapper.getUserList();
}
}
4、控制层实现分页
工具类
public class DataUtil {
/**
* 获取分页数据
* @param list
* @param
* @return
*/
public static Map getPageData(List list){
PageInfo pageInfo = new PageInfo<>(list);
Map data = new HashMap<>();
data.put("total",pageInfo.getTotal());
data.put("list",pageInfo.getList());
return data;
}
}
PageInfo中的一些我们需要用到的参数如下表:
PageInfo.list | 结果集 |
PageInfo.pageNum | 当前页码 |
PageInfo.pageSize | 当前页面显示的数据条目 |
PageInfo.pages | 总页数 |
PageInfo.total | 数据的总条目数 |
PageInfo.prePage | 上一页 |
PageInfo.nextPage | 下一页 |
PageInfo.isFirstPage | 是否为第一页 |
PageInfo.isLastPage | 是否为最后一页 |
PageInfo.hasPreviousPage | 是否有上一页 |
PageHelper.hasNextPage | 是否有下一页 |
Controller增加分页支持
@RestController
@RequestMapping(value = "/user")
@Api(tags = {"Member"})
public class UserController {
@Autowired
private UserService mUserService;
@ApiOperation(value = "获取用户列表")
@RequestMapping(value = "/users",method = RequestMethod.GET)
public ResponseEntity getUserList(@RequestParam(value = "pageIndex",defaultValue = "1") @ApiParam("起始页码") int pageIndex,
@RequestParam(value = "pageSize",required = true,defaultValue = "10") @ApiParam("每页显示数量") int pageSize){
PageHelper.startPage(pageIndex,pageSize);
List list = mUserService.getUserList();
return ResponseEntity.success(DataUtil.getPageData(list));
}
}
=============================================
下面知识记录
分页插件参数介绍
分页插件提供了多个可选参数,这些参数使用时,按照上面两种配置方式中的示例配置即可。
分页插件可选参数如下:
-
dialect
:默认情况下会使用 PageHelper 方式进行分页,如果想要实现自己的分页逻辑,可以实现Dialect
(com.github.pagehelper.Dialect
) 接口,然后配置该属性为实现类的全限定名称。
下面几个参数都是针对默认 dialect 情况下的参数。使用自定义 dialect 实现时,下面的参数没有任何作用。
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
类型)。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
后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定。
重要提示:
当 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
时,数据库连接只会获取一次,所以不需要担心占用的这一个连接是否会导致数据库出错,但是最好也根据数据源的特性选择是否关闭连接。
如何在代码中使用
阅读前请注意看重要提示
分页插件支持以下几种调用方式:
//第一种,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方式的调用
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 = countryMapper.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 = 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
的用法:
//获取第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
参数。 例如下面的配置:
在 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 = countryMapper.selectIf(param1);
} else {
list = new ArrayList();
}
这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
上面这个代码,应该写成下面这个样子:
List list;
if(param1 != null){
PageHelper.startPage(1, 10);
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList();
}
这种写法就能保证安全。
如果你对此不放心,你可以手动清理 ThreadLocal
存储的分页参数,可以像下面这样使用:
List list;
if(param1 != null){
PageHelper.startPage(1, 10);
try{
list = countryMapper.selectAll();
} finally {
PageHelper.clearPage();
}
} else {
list = new ArrayList();
}
这么写很不好看,而且没有必要。