Spring Boot项目通用功能之《通用分页》

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/aiyaya_/article/details/79178984

前言

上一篇文章中我们说了下怎么去使用《通用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

欢迎关注我们的公众号或加群,等你哦!

猜你喜欢

转载自blog.csdn.net/aiyaya_/article/details/79178984