在实际的项目开发中,常常需要使用到分页,分页方式分为两种:前端分页和后端分页。
前端分页:一次ajax
请求数据的所有记录,然后在前端缓存并且计算count
和分页逻辑,一般前端组件(例如dataTable)会提供分页动作。
特点是:简单,很适合小规模的web平台;当数据量大的时候会产生性能问题,在查询和网络传输的时间会很长。
后端分页
在ajax请求中指定页码pageNum
和每页的大小pageSize
,后端查询出当页的数据返回,前端只负责渲染。
特点是:复杂一些;性能瓶颈在MySQL的查询性能,这个当然可以调优解决。一般来说,开发使用的是这种方式。
不使用分页插件的分页操作
在没有使用分页插件的时候需要先写一个查询count
的select
语句,然后再写一个真正分页查询的语句,MySQL中有对分页的支持,是通过limit
子句limit
关键字的用法是:LIMIT [offset,] rows
offset
是相对于首行的偏移量(首行是0),rows
是返回条数。
例如:
每页5条记录,取第一页,返回的是前5条记录select * from tableA limit 0,5;
每页5条记录,取第二页,返回的是第6条记录,到第10条记录,select * from tableA limit 5,5;
不过当偏移量逐渐增大的时候,查询速度可能就会变慢,性能会有所下降。
使用Mybatis分页插件PageHelper
PageHelper是一款好用的开源免费的Mybatis第三方物理分页插件
PageHelper的github地址:https://github.com/pagehelper/Mybatis-PageHelper
在springboot中使用PageHelper
首先要在pom.xml中配置PageHelper的依赖
在http://www.mvnrepository.com/中可以发现pagehelper有4.x和5.x两个版本,用法有所不同,并不是向下兼容,在使用5.x版本的时候可能会报错
<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版本就要这样配置
官方推荐
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>
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>
个人推荐使用第一种,配置如下
<!-- 配置分页插件 -->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 设置数据库类型 Oracle,Mysql,MariaDB,SQLite,Hsqldb,PostgreSQL六种数据库-->
<property name="helperDialect" value="mysql"/>
</plugin>
</plugins>
如果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版本这样配置是不行的
那么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的配置(官方推荐)
项目中使用方法和结果
在配置完mybatis后,我简单的说下pagehelper的业务用法,就以分页查询用户列表为例
添加查询所以用户的mapper
接口,对应的sql语句我就不写了
List<UserVo> listUser();
重点来了,然后在service
中,先开启分页,然后把查询结果集放入PageInfo
中
public PageInfo listUserByPage(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<UserVo> userVoList=userMapper.listUser();
PageInfo pageInfo=new PageInfo(userVoList);
return pageInfo;
}
PageHelper.startPage(pageNum, pageSize);
这句非常重要,这段代码表示分页的开始,意思是从第pageNum
页开始,每页显示pageSize
条记录。PageInfo
这个类是插件里的类,这个类里面的属性会在输出结果中显示,
使用PageInfo
这个类,你需要将查询出来的list
放进去:
PageHelper输出的数据结构
然后在controller
层调用该方法设置对应的pageNum
和pageSize
就可以了,我设置pageNum
为1, pageSize
为5,看个输出结果吧
{
"msg": "获取第1页用户信息成功",
"code": 200,
"data": {
"pageNum": 1,
"pageSize": 5,
"size": 5,
"orderBy": null,
"startRow": 1,
"endRow": 5,
"total": 11,
"pages": 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
}
PageInfo这个类里面的属性:pageNum
当前页pageSize
每页的数量size
当前页的数量orderBy
排序startRow
当前页面第一个元素在数据库中的行号endRow
当前页面最后一个元素在数据库中的行号total
总记录数(在这里也就是查询到的用户总数)pages
总页数 (这个页数也很好算,每页5条,总共有11条,需要3页才可以显示完)list
结果集prePage
前一页nextPage
下一页isFirstPage
是否为第一页isLastPage
是否为最后一页hasPreviousPage
是否有前一页hasNextPage
是否有下一页navigatePages
导航页码数navigatepageNums
所有导航页号navigateFirstPage
导航第一页navigateLastPage
导航最后一页firstPage
第一页lastPage
最后一页
安全性
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<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>();
}
这种写法就能保证安全。