🔊 本文收纳于 ⭐ 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
方法 用于分页查询,具体包含:
-
页码总数
totalPage
的计算 -
页码列表的容错处理。比如总页码只有 8 页,但是我们在地址栏手动修改为 12 页,我们需要它在分页栏高亮最后一页并显示最后一页数据,而不是第 12 页,第 12 页是不存在的。
- 分页查询数据。涉及每页起始索引的计算
- 第 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
自己完成映射
总结
以上。全文应该没啥槽点,分页查询的逻辑并不难,不过自己上手做的话需要兼顾前后端可能还是会出现一些问题,大家最好还是动手做一做。