一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情。
现在的公司没有使用MyBatis-Plus的插件开发,使用的是原生的MyBatis,在做分页方面不太好,里面使用了PageHelper的拦截器处理分页
在实际的项目开发中,常常需要使用到分页,分页方式分为两种:前端分页和后端分页
-
前端分页(假分页)
一次
ajax请求数据的所有记录,然后在前端缓存并且计算count和分页逻辑,一般前端组件(例如dataTable)会提供分页动作。 特点是:简单很适合小规模的web平台,当数据量大的时候会产生性能问题,在查询和网络传输的时间会很长。 -
后端分页(真分页)
在ajax请求中指定页码
pageNum和每页的大小pageSize,后端查询出当页的数据返回,前端只负责渲染。 特点是:复杂一些;性能瓶颈在MySQL的查询性能,这个当然可以调优解决。一般来说,开发使用的是这种方式。
1.不使用分页插件的分页操作
在没有使用分页插件的时候需要先写一个查询count的select语句,然后再写一个真正分页查询的语句,MySQL中有对分页的支持,是通过limit子句
limit关键字的用法是:LIMIT [offset,] rowsoffset是相对于首行的偏移量(首行是0)rows是返回条数(每页显示条数)
例如:
- 每页5条记录,取第一页,返回的是前5条记录
select * from tableA limit 0,5;
- 每页5条记录,取第二页,返回的是第6条记录,到第10条记录
select * from tableA limit 5,5;
不过当偏移量逐渐增大的时候,查询速度可能就会变慢,性能会有所下降(可以优化SQL语句提高效率)。
2.分页插件PageHelper 4.X配置
PageHelper是一款好用的开源免费的Mybatis第三方物理分页插件 Github地址:github.com/pagehelper/… 官方地址:pagehelper.github.io/
示例:在SpringBoot中使用PageHelper
- 首先要在
pom.xml中配置PageHelper的依赖 在www.mvnrepository.com/中可以发现`pageh…
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.2.1</version>
</dependency>
在Mybatis的配置文件中配置PageHelper插件,假如不配置在后面使用PageInfo类时就会出现问题,输出结果的PageInfo属性值基本上都是错的,配置如下
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageHelper">
<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="false"/>
<!-- 设置为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,不配置映射的用默认值 -->
<!--<property name="params" value="pageNum=start;pageSize=limit;pageSizeZero=zero;reasonable=heli;count=contsql"/>-->
</plugin>
</plugins>
上面是PageHelper官方给的配置和注释,虽然写的很多,不过确实描述的很明白。
dialect:标识是哪一种数据库,设计上必须。offsetAsPageNum:将RowBounds第一个参数offset当成pageNum页码使用rowBoundsWithCount:设置为true时,使用RowBounds分页会进行count查询reasonable:value=true时,pageNum小于1会查询第一页,如果pageNum大于pageSize会查询最后一页
注:上面的配置只针对于pagehelper4.x版本的,如果你用的是pagehelper5.x版本就要这样配置
3.官方推荐PageHelper 5.X配置
(1)配置方式1:在 MyBatis 配置 xml 中配置拦截器插件(个人推荐方式)
<!--
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>
示例代码
<!-- 配置分页插件 -->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 设置数据库类型 Oracle,Mysql,MariaDB,SQLite,Hsqldb,PostgreSQL六种数据库-->
<property name="helperDialect" value="mysql"/>
</plugin>
</plugins>
(2)配置方式2:在 Spring 配置文件中配置拦截器插件
<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>
4.MyBatis项目中的应用
在配置完mybatis后,我简单的说下PageHelper的业务用法,就以分页查询用户列表为例,查询所有用户的mapper接口(没有进行分页SQL)
List<UserVo> listUser();// SELECT * FROM sys_user
重点来了,然后在service中,先开启分页,然后把查询结果集放入PageInfo中
public PageInfo listUserByPage(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);// 1. 开启分页
List<UserVo> userVoList=userMapper.listUser();
PageInfo pageInfo=new PageInfo(userVoList);// 2.查询结果集放入`PageInfo`
return pageInfo;
}
代码说明:
PageHelper.startPage(pageNum, pageSize);这句非常重要,这段代码表示分页的开始,意思是从第pageNum页开始,每页显示pageSize条记录。PageInfo这个类是插件里的类,这个类里面的属性会在输出结果中显示,使用PageInfo这个类,你需要将查询出来的list放进去。
PageHelper输出的数据结构,然后在controller层调用该方法设置对应的pageNum和pageSize就可以了,我设置pageNum为1, pageSize为5,看个输出JSON结果
{
"msg": "获取第1页用户信息成功",
"code": 200,
"data": {
"pageNum": 1, // 当前页
"pageSize": 5, // 每页的数量
"size": 5, // 当前页的数量
"orderBy": null, // 排序
"startRow": 1, // 当前页面第一个元素在数据库中的行号
"endRow": 5, // 当前页面最后一个元素在数据库中的行号
"total": 11, // 总记录数(在这里也就是查询到的用户总数)
"pages": 3, // 总页数 (这个页数也很好算,每页5条,总共有11条,需要3页才可以显示完)
"list": [ // 结果集
{
"userId": "a24d0c3b-2786-11e8-9835-e4f89cdc0d1f",
"username": "2015081040"
},
{
"userId": "b0bc9e45-2786-11e8-9835-e4f89cdc0d1f",
"username": "2015081041"
},
{
"userId": "b44fd6ac-2786-11e8-9835-e4f89cdc0d1f",
"username": "2015081042"
},
{
"userId": "b7ac58f7-2786-11e8-9835-e4f89cdc0d1f",
"username": "2015081043"
},
{
"userId": "bbdeb5d8-2786-11e8-9835-e4f89cdc0d1f",
"username": "2015081044"
}
],
"prePage": 0, // 前一页
"nextPage": 2, // 下一页
"isFirstPage": true, // 是否为第一页
"isLastPage": false, // 是否为最后一页
"hasPreviousPage": false, // 是否有前一页
"hasNextPage": true, // 是否有下一页
"navigatePages": 8, // 导航页码数
"navigatepageNums": [ // 所有导航页号
1,
2,
3
],
"navigateFirstPage": 1, // 导航第一页
"navigateLastPage": 3, // 导航最后一页
"firstPage": 1, // 第一页
"lastPage": 3 // 最后一页
},
"success": true,
"error": null
}
5. SpringBoot中的配置
- 引入依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
- 简单使用
//[pageNum, pageSize] 页码 每页显示数量
PageHelper.startPage(pageNum,pageSize);
PageInfo<UserInfo> pageInfo = new PageInfo<>(UserInfoService.selectUserList());
- 进阶说明
官方文档中,提供了很多的参数供我们配置:helperDialect,offsetAsPageNum,rowBoundsWithCount,pageSizeZero,reasonable,params,supportMethodsArguments,autoRuntimeDialect,closeConn等等。
spring boot项目里面,到application.yml中进行配置
pagehelper:
# dialect: (1)
# 分页插件会自动检测当前的数据库链接,自动选择合适的分页方式(可以不设置)
helper-dialect: mysql
# 上面数据库设置后,下面的设置为true不会改变上面的结果(默认为true)
auto-dialect: true
page-size-zero: false # (2)
reasonable: true # (3)
# 默认值为 false,该参数对使用 RowBounds 作为分页参数时有效。(一般用不着)
offset-as-page-num: false
# 默认值为 false,RowBounds是否进行count查询(一般用不着)
row-bounds-with-count: false
#params: (4)
#support-methods-arguments: 和params配合使用,具体可以看下面的讲解
# 默认值为 false。设置为 true 时,允许在运行时根据多数据源自动识别对应方言的分页
auto-runtime-dialect: false # (5)
# 与auto-runtime-dialect配合使用
close-conn: true
# 用于控制默认不带 count 查询的方法中,是否执行 count 查询,这里设置为true后,total会为-1
default-count: false
#dialect-alias: (6)
(1) 默认情况下会使用 PageHelper 方式进行分页,如果想要实现自己的分页逻辑,可以实现 Dialect(com.github.pagehelper.Dialect) 接口,然后配置该属性为实现类的全限定名称。(这里不推荐这样玩,毕竟你用了别人的插件,干嘛还要多此一举呢?)
(2) 默认值为 false,当该参数设置为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型)。
(3) 合法性,即纠错机制,配置reasonable为 true,这时如果 pageNum <= 0 会查询第一页,如果 pageNum > pages 会查询最后一页。
(4) 为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值, 默认值为
pageNum=pageNum;
pageSize=pageSize;
count=countSql;
reasonable=reasonable;
pageSizeZero=pageSizeZero。
support-methods-arguments支持通过 Mapper 接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。
(5) 默认值为false。设置为 true 时,允许在运行时根据多数据源自动识别对应方言的分页,closeConn:默认值为 true。当使用运行时动态数据源或没有设置 helperDialect 属性自动获取数据库类型时,会自动获取一个数据库连接, 通过该属性来设置是否关闭获取的这个连接,默认true关闭,设置为 false 后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定。
(6) dialect-alias 参数,允许配置自定义实现的别名,可以用于根据JDBCURL自动获取对应实现,允许通过此种方式覆盖已有的实现,配置示例如(多个配置用分号;隔开):
pagehelper.dialect-alias=oracle=com.github.pagehelper.dialect.helper.OracleDialect
可以使用Lambda表达式
//1.offsetPage
PageHelper.offsetPage(1, 10);
return PageInfo.of(userService.findAll());
//2.Lambda
return PageHelper.startPage(1, 10).doSelectPageInfo(() -> userService.findAll());
//部分属性
System.out.println("总数量"+pageInfo.getTotal());
System.out.println("当前页查询记录"+pageInfo.getList().size());
System.out.println("当前页码"+pageInfo.getPageNum());
System.out.println("每页显示数量"+pageInfo.getPageSize());
System.out.println("总页"+pageInfo.getPages());
controler中方式
public PageInfo<T> getPageById(@RequestParam("sectionId") String sectionId,@RequestParam("pageNum" Integer pageNum,@RequestParam("pageSize" Integer pageSize)) {
PageInfo<T> info= PageHelper
.orderBy(xxx ASC/DESC)
.startPage(pageNum, pageSize).doSelectPage(() -> {
this.xxxService.getPagedBySectionId(sectionId);
});
return info;
}
//第一种、RowBounds方式的调用
List<Country> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));
//第二种、Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第三种、Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第四种、参数方法调用
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(
@Param("user") User user,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<Country> 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<Country> selectByPageNumSize(User user);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<Country> list = countryMapper.selectByPageNumSize(user);
//第六种、ISelect 接口方式
//jdk6,7用法,创建接口
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectGroupBy();
}
});
//jdk8 lambda用法
Page<Country> 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));
6. PageHelper 安全调用
-
使用
RowBounds和PageRowBounds参数方式是极其安全的 -
使用参数方式是极其安全的
-
使用 ISelect 接口调用是极其安全的
ISelect接口方式除了可以保证安全外,还特别实现了将查询转换为单纯的 count 查询方式,这个方法可以将任意的查询方法,变成一个select count(*)的查询方法。 -
什么时候会导致不安全的分页?
PageHelper方法使用了静态的ThreadLocal参数,分页参数和线程是绑定的。 **只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。**因为 PageHelper 在finally代码段中自动清除了ThreadLocal存储的对象。如果代码在进入Executor前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到MappedStatement时), 这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。\ -
但是如果你写出下面这样的代码,就是不安全的用法:
PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
这种情况下由于 param1 存在null的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
上面这个代码,应该写成下面这个样子:这种写法就能保证安全。
List<Country> list;
if(param1 != null){
PageHelper.startPage(1, 10);
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
附录
(1)如果4.x的版本用了5.x的版本报错信息如下,springboot在启动项目的时候就会报错,报错信息有很多,主要是因为
Caused by: org.apache.ibatis.builder.BuilderException:
Error resolving class. Cause: org.apache.ibatis.type.TypeException:
Could not resolve type alias 'com.github.pagehelper.PageInterceptor'.
Caused by: org.apache.ibatis.type.TypeException:
Could not resolve type alias 'com.github.pagehelper.PageInterceptor'.
Caused by: java.lang.ClassNotFoundException:
Cannot find class: com.github.pagehelper.PageInterceptor
总的来说就是缺少了com.github.pagehelper.PageInterceptor,这个是新版拦截器,5.x版本才开始使用,所以在4.x版本这样配置是不行的。
(2)5.x版本的配置在pagehelper4.x上能生效吗?答案是不行
Caused by: org.apache.ibatis.builder.BuilderException:
Error parsing SQL Mapper Configuration. Cause:
java.lang.ClassCastException: com.github.pagehelper.PageHelper
cannot be cast to org.apache.ibatis.plugin.Interceptor
Caused by: java.lang.ClassCastException:
com.github.pagehelper.PageHelper
cannot be cast to org.apache.ibatis.plugin.Interceptor
新版的拦截器PageInterceptor不能和旧版拦截器相互转换,所以还是不行的。
总结:pagehelper4.x就该用4.x的配置,pagehelper5.x就用5.x的配置(官方推荐)