本文已参与「新人创作礼」活动,一起开启掘金创作之路。
PageHelper 的使用
非 SpringBoot 项目中使用
① 引入依赖
<!-- pageHelper 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.0.0</version>
</dependency>
② 插件设置
XML 方式 : MyBatis.xml
<configuration>
...
<!-- 使用插件 -->
<plugins>
<!-- 使用分页插件 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 分页参数合理化, 当页码数 < 0 时, 显示第一页. 当页码数 > 总页码数时, 显示最后一页 -->
<property name="reasonable" value="true"/>
</plugin>
</plugins>
</configuration>
JavaConfig 方式 : 添加配置类 :
@Configuration
public class PageHelperConfig {
@Bean
public PageHelper pageHelper(){
PageHelper pageHelper = new PageHelper();
Properties p = new Properties();
//1.offsetAsPageNum:设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用.
p.setProperty("offsetAsPageNum", "true");
//2.rowBoundsWithCount:设置为true时,使用RowBounds分页会进行count查询.
p.setProperty("rowBoundsWithCount", "true");
//3.reasonable:启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页。
p.setProperty("reasonable", "true");
pageHelper.setProperties(p);
return pageHelper;
}
}
③ Service 层
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
public List<Employee> getAll(){
EmployeeExample example = new EmployeeExample();
example.setOrderByClause("emp_id asc");
return employeeMapper.selectByExampleWithDept(example);
}
}
④ Controller 层 :
// 查询所有 Employee , 分页查询
@RequestMapping("/emps")
public String getEmps( @RequestParam(value = "pn", defaultValue = "1") Integer pn, Model model) {
System.out.println("请求参数 pn = " + pn);
// 分页查询, 可以引入 PageHelper 分页插件. 在查询之前, 只需要调用 startPage() 方法即可.
// 参数1: 查询开始的页码 . 参数2:每页的大小.
PageHelper.startPage(pn, 5);
// startPage() 方法后面紧跟的这个查询就是一个分页查询.
List<Employee> employees = employeeService.getAll();
// 使用 pageInfo 包装查询后的结果, 封装了十分详细的分页信息, 包括有查询出来的数据, 传入连续显示的页数, .....
// 参数1: 要显示的数据. 参数2: 页码下标连续显示几页.
PageInfo<Employee> page = new PageInfo<>(employees, 5);
model.addAttribute("pageInfo", page);
return "list";
}
SpringBoot 项目中使用
使用方式大同小略, 修改一下依赖和配置即可.
① 引入依赖
<!-- pageHelper 分页插件, SpringBoot 项目中使用 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
② 配置 application.yml
# 分页配置
pagehelper:
helper-dialect: mysql #使用的数据库. mysql, oracle, sqlite
reasonable: true #开启优化,在分页页码结果没有数据的时候,会显示有数据的页码数据,也就是当当前页<1时,返回第 1 页, 当当前页 > 最大页时, 返回最后一页的数据.
support-methods-arguments: true #是否支持接口参数来传递分页参数,默认false
pageSizeZero: false #表示当 pageSize=0 时返回所有
params: count=countSql
③ 使用
Controller :
/**
* 分页获取任务执行结果
* @return List<TaskResultVo>
*/
@GetMapping(value = "page/{pageSize}/{pageNum}")
public Result page(@PathVariable(value = "pageSize") Integer pageSize,
@PathVariable(value = "pageNum") Integer pageNum) {
PageHelper.startPage(pageNum,pageSize);
List<TaskResult> taskResultList = taskResultService.list();
PageInfo<TaskResult> page = new PageInfo<>(taskResultList, 5);
return new Result(true,200,"查询成功", page);
}
Service
/**
* 获取所有的任务执行结果
* @return List<TaskResult>
*/
public List<TaskResult> list() {
TaskResultExample taskResultExample = new TaskResultExample();
taskResultExample.setOrderByClause("id");
return taskResultMapper.selectByExample(taskResultExample);
}
分页类 PageInfo 解释
分页类 PageInfo 的所有属性 :
private int pageNum; // 当前页的号码
private int pageSize; // 每页的数量
private int size; // 当前页的数量
private String orderBy; // 排序
//由于startRow和endRow不常用,这里说个具体的用法
//可以在页面中"显示startRow到endRow 共size条数据"
private int startRow; // 当前页面第一个元素在数据库中的行号
private int endRow; // 当前页面最后一个元素在数据库中的行号
private long total; // 总记录数
private int pages; // 总页数
private List<T> list; // 结果集
private int firstPage; // 第一页
private int prePage; // 前一页
private int nextPage; // 下一页
private int lastPage; // 最后一页
private boolean isFirstPage = false; // 是否为第一页
private boolean isLastPage = false; // 是否为最后一页
private boolean hasPreviousPage = false; // 是否有前一页
private boolean hasNextPage = false; // 是否有下一页
private int navigatePages; // 导航页码数
private int[] navigatepageNums; // 所有导航页号
分页插件需要注意的问题
分页的格式要求比较高 , 如下 :
- 使用 PageHelper.startPage(pageNum,pageSize); 开启分页
- 中间有个获取数据的方法
- 创建 PageInfo pageInfo = new PageInfo<>(taskResultList); 分页对象
关键点在于 中间获取数据的方法, 该方法很容易导致分页返回的总记录数 total 错误.
① 如果中间获取数据的方法, 只是一个简单的从数据库查询就返回, 没有经过其他任何操作的话, 分页是没有问题的:
/**
* 分页获取任务执行结果
* @return List<TaskResult>
*/
@GetMapping(value = "page/{pageSize}/{pageNum}")
public Result page(@PathVariable(value = "pageSize") Integer pageSize,
@PathVariable(value = "pageNum") Integer pageNum) {
PageHelper.startPage(pageNum,pageSize); //开启分页
List<TaskResult> taskResultList = taskResultService.list(); //获取数据
PageInfo pageInfo = new PageInfo<>(taskResultList); //创建分页对象
return new Result(true,200,"查询成功", pageInfo);
}
/**
* 获取所有的任务执行结果
* @return List<TaskResult>
*/
public List<TaskResult> list() {
TaskResultExample taskResultExample = new TaskResultExample();
taskResultExample.setOrderByClause("id");
return taskResultMapper.selectByExample(taskResultExample);
}
② 如果中间获取数据的方法, 从数据库查询数据后, 还还要对查询的数据进行处理, 那么分页结果会有问题的 :
错误使用 :
/**
* 分页获取任务执行结果
* @return List<TaskResultVo>
*/
@GetMapping(value = "page/{pageSize}/{pageNum}")
public Result page(@PathVariable(value = "pageSize") Integer pageSize,
@PathVariable(value = "pageNum") Integer pageNum) {
PageHelper.startPage(pageNum,pageSize); //开启分页
List<TaskResultVo> taskResultVoList = taskResultService.listVo(); //获取数据
PageInfo pageInfo = new PageInfo<>(taskResultVoList); //创建分页对象
return new Result(true,200,"查询成功", pageInfo);
}
/**
* 获取所有的任务执行结果
* @return List<TaskResult>
*/
public List<TaskResultVo> listVo() {
TaskResultExample taskResultExample = new TaskResultExample();
taskResultExample.setOrderByClause("id");
List<TaskResult> taskResultList = taskResultMapper.selectByExample(taskResultExample);
ArrayList<TaskResultVo> taskResultVos = new ArrayList<>();
//获取数据后,进行了处理然后才返回的
taskResultList.forEach(x -> {
TaskResultVo taskResultVo = TaskResultVo.convertToTaskResultVo(x);
taskResultVos.add(taskResultVo);
});
return taskResultVos;
}
但是从数据库获取到的数据, 必须进行处理, 那该怎么办呢?
正确的使用方式 :
===Controller
/**
* 分页获取任务执行结果
* @return List<TaskResultVo>
*/
@GetMapping(value = "page/{pageSize}/{pageNum}")
public Result page(@PathVariable(value = "pageSize") Integer pageSize,
@PathVariable(value = "pageNum") Integer pageNum) {
PageHelper.startPage(pageNum,pageSize); //分页
List<TaskResult> taskResultList = taskResultService.list();
PageInfo pageInfoOld = new PageInfo<>(taskResultList); //先保存分页结果
List<TaskResultVo> taskResultVoList = taskResultService.listVo(taskResultList); //结果集做其他操作
PageInfo<TaskResultVo> pageInfoNew = new PageInfo<>(taskResultVoList); //再次创建分页对象
BeanUtils.copyProperties(pageInfoOld,pageInfoNew,"list"); //复制属性(Spring的)
return new Result(true,200,"查询成功", pageInfoNew);
}
===Service
/**
* 获取所有的任务执行结果
* @return List<TaskResult>
*/
public List<TaskResult> list() {
TaskResultExample taskResultExample = new TaskResultExample();
taskResultExample.setOrderByClause("id");
return taskResultMapper.selectByExample(taskResultExample);
}
/**
* 获取所有的任务执行结果
* @return List<TaskResultVo>
*/
public List<TaskResultVo> listVo(List<TaskResult> list) {
ArrayList<TaskResultVo> taskResultVos = new ArrayList<>();
list.forEach(x -> {
TaskResultVo taskResultVo = TaskResultVo.convertToTaskResultVo(x);
taskResultVos.add(taskResultVo);
});
return taskResultVos;
}
不生效的可能原因
1.版本过低
2.语句顺序不对
PageHelper 里面的 PageHelper.startPage(1,10); 只对该语句以后的第一个查询语句得到的数据进行分页.
比如:
PageHelper.startPage(pageNum, pageSize);
List<BrandBo> brands= brandMapper.getBrand();
3.业务顺序问题
比如,如下的代码,分页是存在问题的:
PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
PageHelper 方法使用了 ThreadLocal 参数,分页参数和线程是绑定的。
只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的,因为 PageHelper 在 finally 代码段中会自动 remove 掉 ThreadLocal 存储的对象。
那么对于上面的代码,如果 param1 == null 时,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上,不会被 remove 掉。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
所以正确的写法应该是下面这样:
List<Country> list;
if(param1 != null){
PageHelper.startPage(1, 10);
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
4.配置了多个分页插件
系统中配置了多个分页插件,导致异常。
5.不支持带有 for update 语句的分页
对于带有 for update 的 sql,会抛出运行时异常,对于这样的 sql 建议手动分页,毕竟这样的 sql 需要重视。
6.左连接不分页问题
详见:cloud.tencent.com/developer/a…
分页插件性能问题
PageHelper 的分页功能是通过 Limit 拼接 SQL 实现的,在大数据量下,这种 limit 分页本来就有性能问题,所以 PageHelper 也就存在了性能问题。
解决办法:手写分页 sql,这就是深度分页问题了。
源码
关键词:ThreadLocal,MyBatis 拦截器
调用 startPage() 方法进行分页时,最终调用的方法如下:
里边最重要的就是 getLocalPage() 和 setLocalPage(page) 方法,他俩是看当前线程中的 ThreadLocal.ThreadLocalMap 中是否存在该 page 对象,若存在直接取出,若不存在则设置一个,我们以第一个为例继续深入。
一说到 sql 的拦截功能,大家应该会想到 Mybatis 的拦截器吧。PageHelper 也是用的 mybatis 的拦截器进行分页的。直接搜索PageInterceptor。代码也比较简单,略过。