1.约定前后端交互接口
请求
/book/getListByPage
参数
currentPage=1&pageSize=10
响应
返回封装的result对象对应的Json数据
2. 整体逻辑
2.1 Controller的逻辑
(1)把接收的参数封装为PageRequest类,里面有属性:currentPage(当前的页码),pageSize(每页设置多少条数据),offset(起始数据的Id)
(2)在PageRequest类中设置构造函数,构造函数里面设置offset的值
(3)检查访问者是否为已登录的用户
(4)检验传来的参数PageResult对象的属性是否合理
(5)把参数传给Service层
2.2 Service层
(1)使用Mapper获取总数据的条数
(2)把得到的参数传给Mapper对应的方法,获取该页的数据
(3)把得到的数据进行处理
(4)返回该Controller
2.3 Mapper层
(1)实现一个方法:查询数据的总数据条数
(2)实现一个方法:使用limit 实现分页查询
3. 后端代码实现
3.1 分页的逻辑和封装的类
(1)假设每页10条数据,逻辑如下:
第一页,第1-10的数据
第二页,第11-20的数据
第三页,第21-30的数据...
(2)数据库中数据的索引是从0开始的,更新逻辑(假设每页10条数据):
第一页,索引为0-9的数据
第二页,索引为10-19的数据
第三页,索引为20-29的数据...
(3)要想实现这个功能, 从数据库中进行分页查询,我们要使用 LIMIT 关键字,格式为:limit 开始索引每页显示的条数(开始索引从0开始)
limit a,b
关键字:a是开始的索引,b是从a开始显示的条数
sql
--第一页
select * from book_info limit 0,10;
--第二页
select * from book_info limit 10,10;
--第三页
select * from book_info limit 20,10;
观察以上SQL语句,发现: 开始索引⼀直在改变, 每每页显示条数是固定的
开始索引的计算公式: 开始索引 = (当前页码 - 1) * 每页显示条数
(3)从上述的分析得出,要实现一个分页的查询需要三个属性:当前页码、查询的条数、起始的索引
(4)创建一个类PageRequest,封装这三个属性。这三个属性的逻辑关系还需要另外的处理(开始索引 = (当前页码 - 1) * 每页显示条数),为了方便调用,直接在构造方法中实现三者的逻辑关系
swift
@Data
public class PageRequest {
// 请求的页数
private int currentPage=1;
//请求的该页条数
private int pageSize=10;
//开始的索引
private int offset;
public PageRequest(int currentPage){
this.currentPage = currentPage;
this.offset=(this.currentPage-1)* this.pageSize;
}
public PageRequest(){
this.offset=(this.currentPage-1)*this.pageSize;
}
public PageRequest(int currentPage, int pageSize){
this.currentPage=currentPage;
this.pageSize=pageSize;
this.offset=(this.currentPage-1)* this.pageSize;
}
//获取开始的索引
public int getOffset(){
return (this.currentPage-1)* this.pageSize;
}
}
构造函数说明:
(1)当前端没有传来页码和该页的条数时,默认使用该类的缺省值
(2)当前端只传来页码而没有该页的条数时,默认使用每页10条数据。
(3)当前端传来页码和该页的条数时,进行逻辑处理求offset。
Controller、Service、传参PageRequest类:
3.2 Controller层
BookInfoController 类的getBookInfoListByPage方法:
java
@Slf4j
@RequestMapping("/book")
@RestController
public class BookInfoController {
@Autowired
private BookInfoService bookInfoService;
// 手动设置拦截用户
@RequestMapping("/getBookInfoListByPage")
public Result getBookInfoListByPage(PageRequest pageRequest, HttpSession session){
log.info("Controller被调用");
//验证是否认证
UserInfo userInfo = (UserInfo)session.getAttribute(Constants.SESSION_USER_KEY);
if(userInfo==null){
Result result = new Result();
result.setStatus(ResultStatus.UNLOGIN);
return result;
}else if(userInfo.getId()<0){
Result result = new Result();
result.setStatus(ResultStatus.UNLOGIN);
}
// 参数校验
if(pageRequest.getPageSize()<1||pageRequest.getCurrentPage()<=0){
Result result = new Result();
result.setStatus(ResultStatus.FIAL);
result.setErrorMessage("参数验证失败");
return result;
}
PageResult<BookInfo> pageResult = null;
try{
pageResult = bookInfoService.getBookInfoListByPage(pageRequest);
}catch(Exception e){
log.error("查询翻页信息错误:" + e);
}
// Result result = new Result();
// result.setStatus(ResultStatus.SUCCESS);
// result.setData(pageResult);
//
// return result;
return Result.success(pageResult);
}
}
说明:
(1)验证是否已认证:如果没有该代码,在浏览器中直接诶搜索127.0.0.1:8080/book/getBookInfoListByPage
(未登录)也会出现图书列表,这是不符合安全设计的。获取session中的对应的用户对象,并检查该用户是否合规。
(2)参数校验:为了高内聚低耦合,封装了一个Result类,Result类中设置的有常用的属性。
(3)页面的结果:为了高内聚低耦合,封装了一个PageResult类,PageResult类中设置的有常用的页面属性。
2.3 PageResult 和 Result
2.3.1 PageResult
如果不设计PageResult类,获取该页的内容后,需要返回前端三个属性:
(1)total(该页数据的总条数);
(2)bookInfoList(该页的数据);
(3)pageRequest(前端的请求内容)。
后端想要一次性返回给前段上述三个属性使用类封装起来是最好的办法,也实现了高内聚低耦合的设计。
PageResult类:
java
@Data
public class PageResult<T> {
//当前页数据的总条数
private int total;
//当前页的数据
private List<T> bookInfoList;
private PageRequest pageRequest;
public PageResult(int count,List<T> bookInfoList, PageRequest pageRequest){
this.total = count;
this.bookInfoList = bookInfoList;
this.pageRequest = pageRequest;
}
}
为了实现了高内聚低耦合的设计,PageResult类设计为模板类;在创建构造函数中设计赋值。
Controller和Service使用PageResult对象:
2.3.2 Result
使用Result类创建的对象可以作为后端返回给前端的中间对象,PageResult类包含返回给前端数据的常用属性。
Result类:
java
public class Result {
private ResultStatus status;
private String ErrorMessage;
private Object data;
public static Result success(Object data){
Result result = new Result();
result.setStatus(ResultStatus.SUCCESS);
// 赋值返回的数据
result.data = data;
return result;
}
public ResultStatus getStatus() {
return status;
}
public void setStatus(ResultStatus status) {
this.status = status;
}
public String getErrorMessage() {
return ErrorMessage;
}
public void setErrorMessage(String errorMessage) {
ErrorMessage = errorMessage;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
说明:
(1)Object 是Java中所有类的根类(基类),使用它可以让 data 字段存储任意类型的数据
(2)status是枚举类ResultStatus 的对象,一共有三个状态:
200:成功--SUCCESS
-1:用户未登录--UNLOGIN
-2:异常或检验失败--FAIL
java
public enum ResultStatus {
SUCCESS(200),
UNLOGIN(-1),
FIAL(-2);
private Integer status;
ResultStatus(Integer Status){
this.status=Status;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
2.4 Service 层
Service层是处理主要业务逻辑的,直接调用Mapper层
java
@Slf4j
@Service
public class BookInfoService {
@Autowired
private BookInfoMapper bookInfoMapper;
public PageResult<BookInfo> getBookInfoListByPage(PageRequest pageRequest) {
if(pageRequest==null){
return null;
}
//获取表中数据的总数
Integer count = bookInfoMapper.queryBookInfoCount();
//获取该页的数据
List<BookInfo> bookInfoList = bookInfoMapper.queryBookInfoByPage(pageRequest);
//根据status 修改 statusCN
for(BookInfo it : bookInfoList){
// 使用枚举类设置图书的状态
it.setStatusCN(BookInfoStatusEnum.getNameByCode(it.getStatus()).getName());
// if(it.getStatus()==0){
// it.setStatusCN("无效");
// } else if(it.getStatus()==1){
// it.setStatusCN("可借阅");
// }else{
// it.setStatusCN("不可借阅");
// }
}
// 创建PageResult对象返回给Controller
return new PageResult<BookInfo>(count,bookInfoList,pageRequest);
}
}
说明:
(1)bookInfoMapper.queryBookInfoCount():获取数据的总条数
(2)queryBookInfoByPage(pageRequest):获取该页的数据
(3)it.setStatusCN(BookInfoStatusEnum.getNameByCode(it.getStatus()).getName()):BookInfoStatusEnum是一个枚举类,该枚举类设置的有图书的状态:
java
public enum BookInfoStatusEnum {
DELETED(0,"已经删除"),
NORMALL(1,"允许借阅"),
FORBIDDEN(2,"禁止借阅");
public static BookInfoStatusEnum getNameByCode(int code){
switch(code) {
case 0: return BookInfoStatusEnum.DELETED;
case 1: return BookInfoStatusEnum.NORMALL;
case 2: return BookInfoStatusEnum.FORBIDDEN;
default:
return BookInfoStatusEnum.FORBIDDEN;
}
}
private int code;
private String name;
//构造函数
BookInfoStatusEnum(int code, String name){
this.code= code;
this.name = name;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
枚举类中的方法getNameByCode是一个静态方法,接收的参数是整型(方便BookInfo的status属性传参),方法中设置switch--case(多条件分支控制的语句),根据传来的参数创建BookInfoStatusEnum对象。
2.5 Mapper层
Mapper直访问数据库,该功能下有两个主要的方法:
java
@Mapper
public interface BookInfoMapper {
//每页的数据
@Select("select id,book_name,author,count,price,publish,status," +
"create_time,update_time from book_info "+
"where status !=0 order by id asc limit #{offset},#{pageSize} "
)
List<BookInfo> queryBookInfoByPage(PageRequest pageRequest);
//数据的总条数
@Select("select count(*) from book_info where status!=0;")
Integer queryBookInfoCount();
说明:
(1)queryBookInfoByPage方法主要使用关键字limit
来进行分页查询,offset是数据的开始id,pageSize是数据条数的数量。
(2)queryBookInfoCount方法用于查询数据的总条数。
4. 前端代码
book_list.html文件:
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图书列表展示</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/list.css">
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/bootstrap.min.js"></script>
<script src="js/jq-paginator.js"></script>
</head>
<body>
<div class="bookContainer">
<h2>图书列表展示</h2>
<div class="navbar-justify-between">
<div>
<button class="btn btn-outline-info" type="button" onclick="location.href='book_add.html'">添加图书</button>
<button class="btn btn-outline-info" type="button" onclick="batchDelete()">批量删除</button>
</div>
</div>
<table>
<thead>
<tr>
<td>选择</td>
<td class="width100">图书ID</td>
<td>书名</td>
<td>作者</td>
<td>数量</td>
<td>定价</td>
<td>出版社</td>
<td>状态</td>
<td class="width200">操作</td>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div class="demo">
<ul id="pageContainer" class="pagination justify-content-center"></ul>
</div>
<script>
getBookList();
function getBookList() {
$.ajax({
// 获取参数
url:"/book/getBookInfoListByPage"+ location.search,
type:"get",
success:function(result){
if(result.status == "FAIL"){
location.href = "login.html";//
}
if(result.status == "UNLOGIN"){
location.href = "login.html";//
}
var finalHtml="";
result = result.data;//
//加载列表
for(var book of result.bookInfoList){
//根据每一条记录去拼接,拼成一个<tr>内容</tr>
finalHtml += '<tr>'
finalHtml += '<td><input type="checkbox" name="selectBook" value="'+book.id+'" id="selectBook" class="book-select"></td>'
finalHtml += '<td>'+book.id+'</td>'
finalHtml += '<td>'+book.bookName+'</td>'
finalHtml += '<td>'+book.author+'</td>'
finalHtml += '<td>'+book.count+'</td>'
finalHtml += '<td>'+book.price+'</td>'
finalHtml += '<td>'+book.publish+'</td>'
finalHtml += '<td>'+book.statusCN+'</td>'
finalHtml += '<td><div class="op">'
finalHtml += '<a href="book_update.html?bookId='+book.id+'">修改</a>'
finalHtml += '<a href="javascript:void(0)" onclick="deleteBook('+book.id+')">删除</a>'
finalHtml += '</div></td></tr>'
}
//展示
console.log(finalHtml);
//放到tbody中
$("tbody").html(finalHtml)
//翻页信息
$("#pageContainer").jqPaginator({
totalCounts: result.total, //总记录数
pageSize: result.pageRequest.pageSize, //每页的个数
visiblePages: 5, //可视页数
currentPage: result.pageRequest.currentPage, //当前页码
first: '<li class="page-item"><a class="page-link">首页</a></li>',
prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
//页面初始化和页码点击时都会执行
onPageChange: function (page, type) {
console.log("第" + page + "页, 类型:" + type);
if(type=="change"){
location.href="book_list.html?currentPage="+page; //使用后端默认设置的每页数据量
}
//跳转
// location.href="book_list.html?currentPage="+page;
}
});
}
});
}
function deleteBook(id) {
var isDelete = confirm("确认删除?");
if (isDelete) {
//删除图书
$.ajax({
//..
});
}
}
function batchDelete() {
var isDelete = confirm("确认批量删除?");
if (isDelete) {
//获取复选框的id
var ids = [];
$("input:checkbox[name='selectBook']:checked").each(function () {
ids.push($(this).val());
});
//发送请求,批量删除
$.ajax({
//..
});
}
}
</script>
</div>
</body>
</html>
ajax说明:
- 处理请求结果
html
if (result.status == "FAIL" || result.status == "UNLOGIN") {
location.href = "login.html"; // 状态异常时跳转到登录页
}
if(result.status == "UNLOGIN"){
location.href = "login.html";//未登录时跳转到登录页
}
- 动态生成表格行
遍历 result.bookInfoList 中的每本图书,生成包含复选框、图书信息和操作按钮的表格行,把每个图书信息拼接一起。
html
var finalHtml = "";
result = result.data; // 提取核心数据(假设 result.data 包含 bookInfoList 和 total)
for (var book of result.bookInfoList) {
finalHtml += '<tr>'
+ '<td><input type="checkbox" name="selectBook" value="' + book.id + '" class="book-select"></td>'
+ '<td>' + book.id + '</td>'
+ '<td>' + book.bookName + '</td>'
+ '<td>' + book.author + '</td>'
+ '<td>' + book.count + '</td>'
+ '<td>' + book.price + '</td>'
+ '<td>' + book.publish + '</td>'
+ '<td>' + book.statusCN + '</td>'
+ '<td><div class="op">'
+ '<a href="book_update.html?bookId=' + book.id + '">修改</a>'
+ '<a href="javascript:void(0)" onclick="deleteBook(' + book.id + ')">删除</a>'
+ '</div></td></tr>';
}
$("tbody").html(finalHtml); // 将拼接的 HTML 插入表格
- 初始化分页控件 (jqPaginator)
html
$("#pageContainer").jqPaginator({
totalCounts: result.total, // 总记录数
pageSize: result.pageRequest.pageSize, // 每页显示数量
visiblePages: 5, // 显示的分页按钮数
currentPage: result.pageRequest.currentPage, // 当前页码
// 分页按钮模板
first: '<li class="page-item"><a class="page-link">首页</a></li>',
prev: '<li class="page-item"><a class="page-link">上一页</a></li>',
next: '<li class="page-item"><a class="page-link">下一页</a></li>',
last: '<li class="page-item"><a class="page-link">末页</a></li>',
page: '<li class="page-item"><a class="page-link">{{page}}</a></li>',
// 页码变化回调
onPageChange: function (page, type) {
if (type == "change") { // 仅在用户点击时跳转
location.href = "book_list.html?currentPage=" + page;
}
}
});
分页参数依赖后端返回的 total(总记录数)、pageRequest.pageSize(每页大小)、pageRequest.currentPage(当前页)。当用户点击分页按钮(类型为 "change")时,通过修改 URL 的 currentPage 参数重新加载页面。
5.结语
本文系统讲解了基于 Spring Boot 和 MyBatis 的分页查询功能实现方案,从前端交互到数据库查询构建了完整的技术链路。通过 模块化封装 与 动态参数处理,实现了高复用性、易维护的分页架构,为数据密集型系统的开发提供了标准化解决方案。
核心价值:
-
分层解耦设计
通过 PageRequest 参数封装、PageResult 分页结果包装、Result 统一响应体,实现前后端数据流的规范化传递。
-
高效分页机制
基于 MySQL 的 LIMIT 分页语法,结合后端动态计算偏移量,有效平衡性能与开发效率。
-
安全与健壮性
参数校验、登录态拦截、异常捕获三重机制保障系统稳定性,避免非法请求与空指针异常。
-
前端交互友好
整合 jqPaginator 分页控件,实现页码动态渲染与无刷新跳转,提升用户体验。