前言
上一篇文章中我们说了下怎么去使用《通用Mapper》来实现对单表的增删改查功能,本篇我就带你学习下,如何使用PageHelper插件来实现对单表的分页功能,至此你就不需要再自己去写一大堆的mapper.xml代码去实现单表基础的功能了(其实我们绝大部分的业务都是基于单表操作的)。
实现目标
先来看一下我们约定的分页参数和响应结果的样子。
分页请求参数:
localhost:8080/zhuma-demo/users?pageNum=1&pageSize=5&orderBy=create_time desc
响应结果:
{
"code": 1,
"msg": "成功",
"data": {
"pageNum": 1,
"pageSize": 5,
"total": 12,
"pages": 3,
"list": [
{
"id": "0c13ec7a-0309-11e8-9139-94de80fdfd41",
"nickname": "小竹马11",
"gender": "MALE",
"type": "NORMAL",
"status": "ENABLED",
"createTime": 1517019792000,
"updateTime": 1517019792000
},
{
"id": "09c70b22-0309-11e8-9139-94de80fdfd41",
"nickname": "小竹马10",
"gender": "MALE",
"type": "NORMAL",
"status": "ENABLED",
"createTime": 1517019788000,
"updateTime": 1517019788000
},
//省略
]
}
}
备注
- 参数pageNum代表当前页号,pageSize代表一页的数量。
- 参数orderBy用来控制排序(由两部分组成:第一个值要排序的字段,第二个是排序方式),其实这个跟mysql的order by语句语法是一样的。
- 返回结果中total代表记录总数,pages代表总页数。
- 返回结果中list是数据列表,当没有查询到数据时,返回[],空数组。
我们先演示了下做该分页功能后最终要实现的目标或者说效果,下面就让我们一起学习下,该怎么完成上述的分页功能吧。
说说分页
通常设计分页接口参数,常用的有两种方式用两个参数两控制分页:
- pageNum和pageSize
- offect和limit
他们的换算关系式:offect = (pageNum - 1) * pageSize; limit=pageSize,所以两种参数方式都是OK也都是很好用的,那我们今天使用的就是pageNum和pageSize这种参数形式,分别代表当前页号和一页的数量,这种参数也更方便理解。然后我们在具体讲解怎么做通用分页。
实现思路
1. 首先引入jar包(使用pageHelper插件进行分页):
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.2</version><!--使用前请获取最新版本-->
</dependency>
如果是spring boot项目,可以直接引入:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
Page-helper插件github地址:https://github.com/pagehelper/Mybatis-PageHelper/
2. 几个核心类
- PageQO:自定义类,用于统一封装查询参数,QO译为query object
- PageVO:自定义类,用于统一封装分页响应数据,VO译为view object
- PageHelper:分页插件提供,该helper类用于做核心分页功能
- Page:分页插件提供,helper返回值,也同时存在数据库查询出来的数据列表信息
3. 以分页查看用户功能为例
我们自己定义的PageQO类(统一查询参数):
package com.zhuma.demo.comm.model.qo;
import org.hibernate.validator.constraints.Range;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @desc 分页查询对象
* @author zhuamer
* @since 7/6/2017 2:48 PM
*/
@ApiModel("分页查询对象")
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageQO<T> {
/**
* 按创建时间倒序排序
*/
public static final String ORDER_BY_CREATE_TIME_DESC = "create_time desc";
@ApiModelProperty(value = "当前页号")
@Range(min = 1, max = Integer.MAX_VALUE)
private int pageNum = 1;
@ApiModelProperty(value = "一页数量")
@Range(min = 1, max = Integer.MAX_VALUE)
private int pageSize = 10;
@ApiModelProperty(value = "排序", notes = "例:create_time desc,update_time desc")
private String orderBy;
private T condition;
public PageQO(int pageNum, int pageSize) {
super();
this.pageNum = pageNum;
this.pageSize = pageSize;
}
public int getOffset() {
return (this.pageNum - 1) * this.pageSize;
}
}
我们自己定义的PageVO类(统一封装分页响应数据):
package com.zhuma.demo.comm.model.vo;
import java.util.List;
import com.zhuma.demo.comm.model.Model;
import com.zhuma.demo.comm.model.qo.PageQO;
import com.zhuma.demo.util.BeanUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageInfo;
import com.google.common.collect.Lists;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @desc 分页VO对象
* @author zhuamer
* @since 7/6/2017 2:48 PM
*/
@ApiModel("分页对象")
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageVO<T> implements Model {
private static final long serialVersionUID = -4426958360243585882L;
@ApiModelProperty(value = "当前页号")
private int pageNum;
@ApiModelProperty(value = "每页的数量")
private int pageSize;
@ApiModelProperty(value = "总记录数")
private long total;
@ApiModelProperty(value = "总页数")
private int pages;
@ApiModelProperty(value = "结果集")
private List<T> list;
public PageVO(PageQO pageQO) {
this.setPageNum(pageQO.getPageNum());
this.setPageSize(pageQO.getPageSize());
}
public PageVO(List<T> poList) {
BeanUtils.copyProperties(new PageInfo<>(poList), this);
}
public static <T> PageVO<T> build(List<T> poList) {
return new PageVO<>(poList);
}
/**
* @desc 构建一个分页VO对象
*
* @param page 数据库查出来的分页数据列表
*/
public static <T> PageVO<T> build(Page<T> page) {
PageVO<T> pageVO = new PageVO<>();
BeanUtils.copyProperties(page.toPageInfo(), pageVO);
return pageVO;
}
/**
* @desc 构建一个分页VO对象
* 试用场景为:从数据库取出的PO列表不做任何处理,转化为VO列表返回
*
* @param page 数据库查出来的分页数据列表
* @param voClazz 要转为的VO类
*/
public static <T, E> PageVO<T> build(Page<E> page, Class<T> voClazz) {
PageVO<T> pageVO = new PageVO<>();
BeanUtils.copyProperties(page, pageVO, "list");
try {
List<T> VOs = Lists.newArrayList();
List<E> POs = page.getResult();
if (!CollectionUtils.isEmpty(POs)) {
for (E e : POs) {
T t = voClazz.newInstance();
BeanUtils.copyProperties(e, t);
VOs.add(t);
}
}
pageVO.setList(VOs);
} catch (IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
}
return pageVO;
}
/**
* @desc 构建一个分页VO对象
* 试用场景为:将处理好的VO列表封装返回
*
* @param poPage 数据库查出来的分页数据
* @param voList vo数据列表
*/
public static <T, E> PageVO<T> build(Page<E> poPage, List<T> voList) {
PageVO<T> page = new PageVO<>();
BeanUtil.copyProperties(poPage, page, "list");
page.setList(voList == null ? Lists.newArrayList() : voList);
return page;
}
public static int getPages(long total, int pageSize) {
if (total == 0 || pageSize == 0) {
return 0;
}
return (int) (total % pageSize == 0 ? (total / pageSize) : (total / pageSize + 1));
}
public int getPages(){
return getPages(this.total, this.pageSize);
}
}
用户Controller类(其中getPage方法就是我们本章的核心啦):
package com.zhuma.demo.web.user;
import java.util.Date;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.zhuma.demo.annotation.ResponseResult;
import com.zhuma.demo.comm.model.qo.PageQO;
import com.zhuma.demo.comm.model.vo.PageVO;
import com.zhuma.demo.comm.result.DefaultErrorResult;
import com.zhuma.demo.comm.result.PlatformResult;
import com.zhuma.demo.comm.result.Result;
import com.zhuma.demo.exception.BusinessException;
import com.zhuma.demo.exception.DataNotFoundException;
import com.zhuma.demo.exception.UserNotLoginException;
import com.zhuma.demo.mapper.UserMapper;
import com.zhuma.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.zhuma.demo.model.po.User;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @desc 用户管理控制器
*
* @author zhumaer
* @since 6/20/2017 16:37 PM
*/
@ResponseResult
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping
public PageVO<User> getPage(PageQO pageQO) {
Page<User> page = PageHelper.startPage(pageQO.getPageNum(), pageQO.getPageSize(), pageQO.getOrderBy());
userMapper.selectAll();
return PageVO.build(page);
}
}
分析讲解
上面就是我们利用pageHelper实现的一个用户的分页功能啦(selectAll方法是使用通用mapper的),调用方法的结果在上面实现目标中已经展示出来啦,是不是还算蛮简单呢,不需要写任何的mapper类和mapper.xml就可以实现了,不过你可能有点看不太懂了,下面我已我自己的使用前的疑问依依解答一下:
① 该分页方式是内存分页还是物理分页?
答:pagehelper分页是属于物理分页的,不是内存分页,所以不用担心数据量过大导致的性能问题。
② userMapper.selectAll()这个方法什么都没有返回,为什么直接使用page就可以得到结果呢?
答:其实你在调用PageHelper.startPage静态方法的时候,PageHelper会把每一次调用的page参数放入ThreadLocal变量中,因为Threadlocal本身就是线程安全的,所以在多线程环境下,各个Threadlocal之间相互隔离,不同thread可以使用不同的数据源或执行不同的SQL语句,PageHelper利用这一点通过拦截器获取到同一线程中的预编译好的SQL语句之后将SQL语句包装成具有分页功能的SQL语句,并将其再次赋值给下一步操作,所以实际执行的SQL语句就是有了分页功能的SQL语句,执行成功后,数据结果会被写回Threadlocal的page变量中。
③ 这种PageHelper.startPage 静态方法的方式,使用的规则或者说我们要怎么具体的使用它呢?
答:使用起来很简单,你只需在你需要进行分页的 MyBatis 查询方法前调用 PageHelper.startPage 静态方法即可,紧跟在这个方法后的第一个MyBatis 查询方法会被进行分页。
④ 使用这种分页方式需要注意什么呢?
答: 使用了PageHelper.startPage方法后,一定要跟一个查询数据库的操作,否则会导致不安全的分页,所谓的不安全分页,比如说在你的方法中使用了该startPage静态方法,但是后面却没有查询数据库操作,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
课外扩展
学过了通用mapper和page-helper的使用后,我们其实可以直接写出一个相对比较通用的分页方法,下面给出代码,留给同学们课后回味和研究下它的好用之处吧O(∩_∩)O~
public PageVO<E> selectPage(PageQO<?> pageQO) {
Assert.notNull(pageQO, "pageQO is not null");
Page<E> page = PageHelper.startPage(pageQO.getPageNum(), pageQO.getPageSize(), pageQO.getOrderBy());
try {
Object condition = pageQO.getCondition();
if (condition == null) {
crudMapper.selectAll();
} else if (condition instanceof Condition) {
crudMapper.selectByCondition(condition);
} else if (condition instanceof Example) {
crudMapper.selectByExample(condition);
} else if (poType.isInstance(condition)){
crudMapper.select((E)condition);
} else {
try {
E e = poType.newInstance();
BeanUtil.copyProperties(condition, e);
crudMapper.select(e);
} catch (InstantiationException | IllegalAccessException e) {
log.error("selectPage occurs error, caused by: ", e);
throw new RuntimeException("poType.newInstance occurs InstantiationException or IllegalAccessException", e);
}
}
} finally {
page.close();
}
return PageVO.build(page);
}
最后
关于通用分页就讲解到这里,下一篇文章会介绍在项目中如果实现通用Service,不知道你对该篇文章还有什么疑惑呢,可以关注我的微信号或留言,很愿意和你一起讨论学习O(∩_∩)O~
最后附上github地址:https://github.com/zhumaer/zhuma/tree/master/zhuma-demo
QQ群号:629446754(欢迎加群)
欢迎关注我们的公众号或加群,等你哦!