使用 PageHelper 前先学会手写一个分页查询吧

1,060 阅读5分钟

🔊 本文收纳于 ⭐ CS-Wiki(Gitee 推荐项目), 欢迎 star ~ 😊


前言

当今框架层出不穷,程序员几乎不得不以年为单位疯狂更新技术栈,但万变不离其宗,了解这些框架的底层原理,才能够更好的掌握框架,而不是被不断迭代的框架所支配

分页是日常开发中很常见的需求,本文带大家基于 MyBatis 和 Spring Boot 一步一步写一个分页查询,了解分页查询的基本逻辑。当然,大家熟悉之后,日常开发中还是推荐利用插件/框架来提高编码效率(比如非常优秀的 MyBatis 分页插件 PageHelper)。

1. 从分页效果入手讲解该如何做

我们要实现的分页效果如下:

分页栏显示当前页以及当前页的前三页和后三页。

分页栏中的页码我们会用一个 ArrayList 存储起来,表示在当前页能够跳转哪些页码。比如当前页是 5 的时候,ArrayList 中就会存 [2,3,4,5,6,7,8], 当前页是 3 的时候,ArrayList 中就会存 [1,2,3,4,5,6]

💦 分页栏中的第一页按钮 << 和最后一页按钮 >> 的基本显示逻辑如下:

  • 当页码列表 ArrayList 中不包含第一页的页码(比如 1)的时候,就显示第一页按钮,否则不显示
  • 当页码列表 ArrayList 中不包含最后一页的页码(比如 8) 的时候,就显示最后一页按钮,否则不显示

💦 分页栏中的上一页按钮 <,下一页按钮 > 的基本显示逻辑如下:

  • 当前页如果不是第一页,就显示上一页按钮,否则不显示
  • 当前页如果不是最后一页,就显示下一页按钮,否则不显示

至于后端如何获取到当前页码,很简单,我们在请求的 url 后面加上表示当前页码的 pageNumber 字段,利用 Spring Boot 的注解 @RequestParam("pageNumber") 就可以获取当前页码了

2. 用于分页的 SQL 语法

分页的关键就是这个 SQL 语句:

select * from tableName limit offset size

这条语句的含义是:从 tableName 表中的第 offset 条记录开始(注意 offset 从零开始),查询出 size 个记录。

举个例子,category 表中数据如下:

运行下边这条语句:查询从第 2 条记录(即 id = 3)开始的连续 5 条记录

select * from category limit 2, 5;

3. 承载分页信息的实体类

分页信息实体类包含如下数据:

  • 是否显示上一页按钮 showPrevious
  • 是否显示下一页按钮 showNext
  • 是否显示跳转到第一页按钮 showFirstPage
  • 是否显示跳转到最后一页按钮 showEndPage
  • 当前页码 pageNumber
  • 当前页可跳转的页码列表 pageNumbers
  • 总页码数 totalPage
  • 当前页要显示的具体数据
/**
 * 分页所需要的信息
 */
public class PaginationDTO {
    private boolean showPrevious; // 是否显示上一页按钮
    private boolean showNext; // 是否显示下一页按钮
    private boolean showFirstPage; // 是否显示跳转到第一页按钮
    private boolean showEndPage; // 是否显示跳转到最后一页按钮

    private Integer pageNumber; // 当前页码
    private List<Integer> pageNumbers = new ArrayList<>(); // 当前页可跳转的页码列表
    private Integer totalPage; // 总页码数
    
    private List<Question> questionList; // 当前页要显示的具体数据
    
    // Getter and Setter

4. 根据当前页码设置分页所需要的信息

这段代码做的事情就是我们在第 1 节分析的,决定上一页、下一页、第一页和最后一页按钮是否显示。

显然,这段代码应该放在承载分页信息的实体类 PaginationDTO 中:

/**
 * 分页所需要的信息
 */
@Data
public class PaginationDTO {

    ......

    /**
     * 根据当前页码设置相关分页信息
     * @param totalPage 页码总数
     * @param pageNumber 当前页码
     */
    public void setPagination(Integer totalPage, Integer pageNumber) {
        this.totalPage = totalPage;
        this.pageNumber = pageNumber;

        // 页码列表的显示:显示当前页和当前页的前三页和后三页,可为空
        pageNumbers.add(pageNumber);
        for (int i = 1; i <= 3; i++) {
            if (pageNumber - i > 0) {
                pageNumbers.add(0, pageNumber - i);
            }

            if (pageNumber + i <= totalPage) {
                pageNumbers.add(pageNumber + i);
            }
        }

        // 是否展示上一页按钮
        if (pageNumber == 1) {
            showPrevious = false;
        }
        else {
            showPrevious = true;
        }
        // 是否展示下一页按钮
        if (pageNumber.equals(totalPage)) {
            showNext = false;
        }
        else {
            showNext = true;
        }

        // 是否展示跳转到第一页按钮
        if (pageNumbers.contains(1)) {
            // 如果当前页可跳转的页码列表包含第一页,则不展示跳转到第一页按钮
            showFirstPage = false;
        }
        else {
            showFirstPage = true;
        }
        // 是否展示跳转到最后一页按钮
        if (pageNumbers.contains(totalPage)) {
            // 如果当前页可跳转的页码列表包含最后一页,则不展示跳转到最后一页按钮
            showEndPage = false;
        }
        else {
            showEndPage = true;
        }
    }
}

5. 分页查询请求入口

Controller 层:

@Controller
public class IndexController {

    @Autowired
    private QuestionService questionService;

    /**
     * @param request
     * @param model
     * @param pageNumber 第多少页,默认第 1 页
     * @param pageSize 每页显示的问题数量, 默认为 10
     * @return
     */
    @GetMapping("/")
    public String index(HttpServletRequest request, Model model,
                        @RequestParam(name = "pageNumber", defaultValue = "1") Integer pageNumber,
                        @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
        
        .........

        // 获取问题列表需要的信息
        PaginationDTO pagination = questionService.list(pageNumber, pageSize);
        model.addAttribute("pagination", pagination);

        ......
    }
}

对应的前端:(Thymeleaf 模板引擎),利用 th:each 标签循环页码列表,然后在 url 后面加上 pageNumber,使得后端能够通过 RequestParam 获取到当前页码

<li th:each="pageNumber : ${pagination.pageNumbers}" th:class="${pagination.pageNumber == pageNumber}? 'active' : ''">
    <a th:href="@{/(pageNumber = ${pageNumber})}" th:text="${pageNumber}"></a>
</li>

上一页、下一页、第一页和最后一页按钮的前端代码就不贴了,大同小异。

QuestionService 如下 👇

6. 分页查询

前面说过,页面承载的信息是 Question,我们建立一个 QuestionService.list 方法 用于分页查询,具体包含:

  1. 页码总数 totalPage 的计算

  2. 页码列表的容错处理。比如总页码只有 8 页,但是我们在地址栏手动修改为 12 页,我们需要它在分页栏高亮最后一页并显示最后一页数据,而不是第 12 页,第 12 页是不存在的。

  1. 分页查询数据。涉及每页起始索引的计算
  • 第 1 页:起始索引 0,limit 0, 10
  • 第 2 页:起始索引 10,limit 10, 20
  • 第 3 页:起始索引 30,limit 20, 30
  • ......

推出 => 若当前页码为 i,每页显示 10 条数据,则每页的起始索引 offset = 10 * (i - 1)

@Service
public class QuestionService {

    @Autowired
    private QuestionMapper questionMapper;

    /**
     * 分页查询
     * @param pageNumber 第多少页(当前页码)
     * @param pageSize 每页显示的问题数量
     * @return
     */
    public PaginationDTO list(Integer pageNumber, Integer pageSize) {

        PaginationDTO paginationDTO = new PaginationDTO();
        Integer totalCount = questionMapper.count(); // 问题总数
        Integer totalPage; // 页码总数
		
        // 计算页码总数
        if (totalCount % pageSize == 0) {
            totalPage = totalCount / pageSize;
        }
        else {
            totalPage = totalCount / pageSize + 1;
        }

        // 页码列表的容错处理
        if (pageNumber < 1) {
            pageNumber = 1;
        }
        if(pageNumber > totalPage) {
            pageNumber = totalPage;
        }

        paginationDTO.setPagination(totalPage, pageNumber);

        Integer offset = pageSize * (pageNumber - 1); // 每页的起始索引
        List<Question> questions = questionMapper.list(offset, pageSize); // 分页查询当前页的具体数据
        paginationDTO.setQuestionList(questions); // 存储当前页的具体数据
        return paginationDTO;
    }
}

对应的 MyBatis 的 mapper 文件 QuestionMapper 如下:

@Select("select * from question limit #{offset}, #{pageSize}")
List<Question> list(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize);

🚨 MyBatis 约定当我们传入的参数不是实体类对象的时候,需要利用 @Param 自己完成映射

总结

以上。全文应该没啥槽点,分页查询的逻辑并不难,不过自己上手做的话需要兼顾前后端可能还是会出现一些问题,大家最好还是动手做一做。