后端分页接口实现
分页查询学习文档
文档概述
创建时间: 2025-07-24
文档版本: v1.0
适用范围: Spring Boot + MyBatis + PageHelper 分页查询实现
技术栈: Spring Boot 2.7.18 + PageHelper + PageObject 继承模式
1. 分页查询架构设计
1.1 整体架构
Controller 层 (接收分页参数)↓
Service 层 (业务逻辑处理)↓
Mapper 层 (数据库查询)↓
PageHelper (分页插件)↓
数据库 (执行分页SQL)
1.2 核心组件
- PageObject: 分页基类,包含分页和排序字段
- PageHelper: MyBatis分页插件
- PageInfo: 分页结果包装类
- ResponseResult: 统一响应格式
- PageResult: 分页响应数据结构
2. PageObject 基类设计
2.1 PageObject 类结构
package com.hn.shengdun.base;import java.io.Serializable;/*** 分页查询基类* 所有分页请求类都应该继承此类*/
public class PageObject implements Serializable {/*** 页码(从1开始)*/private Integer pageNum;/*** 页大小*/private Integer pageSize;/*** 排序字段*/private String orderByColumn;/*** 排序方向(asc/desc)*/private String isAsc;// getter/setter 方法public Integer getPageNum() {return pageNum;}public void setPageNum(Integer pageNum) {this.pageNum = pageNum;}public Integer getPageSize() {return pageSize;}public void setPageSize(Integer pageSize) {this.pageSize = pageSize;}public String getOrderByColumn() {return orderByColumn;}public void setOrderByColumn(String orderByColumn) {this.orderByColumn = orderByColumn;}public String getIsAsc() {return isAsc;}public void setIsAsc(String isAsc) {this.isAsc = isAsc;}
}
2.2 字段说明
字段名 | 类型 | 说明 | 默认值 | 示例 |
---|---|---|---|---|
pageNum | Integer | 页码(从1开始) | 1 | 1, 2, 3 |
pageSize | Integer | 每页记录数 | 10 | 10, 20, 50 |
orderByColumn | String | 排序字段名 | null | “create_time”, “dict_id” |
isAsc | String | 排序方向 | “desc” | “asc”, “desc” |
3. 分页请求类设计
3.1 设计原则
- 继承 PageObject: 获得统一的分页字段
- 实现 Serializable: 支持序列化
- 包含业务字段: 支持条件查询
- 设置默认值: 提供合理的默认分页参数
3.2 字典数据分页请求类示例
package com.hn.bitao.defined.report.module.dto;import com.hn.shengdun.base.PageObject;
import java.io.Serializable;/*** 字典数据分页查询请求DTO* * @author 系统生成*/
public class DictDataPageRequest extends PageObject implements Serializable {/*** 字典编码(精确查询)*/private Long dictCode;/*** 字典标签(模糊查询)*/private String dictLabel;/*** 字典键值(模糊查询)*/private String dictValue;/*** 字典类型(模糊查询)*/private String dictType;/*** 状态(0正常 1停用)*/private String status;/*** 创建者(模糊查询)*/private String createBy;/*** 开始时间(创建时间范围查询)*/private String beginTime;/*** 结束时间(创建时间范围查询)*/private String endTime;// 构造函数public DictDataPageRequest() {super();// 设置默认值this.setPageNum(1);this.setPageSize(10);this.setIsAsc("desc");}// Getter and Setter methodspublic Long getDictCode() {return dictCode;}public void setDictCode(Long dictCode) {this.dictCode = dictCode;}public String getDictLabel() {return dictLabel;}public void setDictLabel(String dictLabel) {this.dictLabel = dictLabel;}public String getDictValue() {return dictValue;}public void setDictValue(String dictValue) {this.dictValue = dictValue;}public String getDictType() {return dictType;}public void setDictType(String dictType) {this.dictType = dictType;}public String getStatus() {return status;}public void setStatus(String status) {this.status = status;}public String getCreateBy() {return createBy;}public void setCreateBy(String createBy) {this.createBy = createBy;}public String getBeginTime() {return beginTime;}public void setBeginTime(String beginTime) {this.beginTime = beginTime;}public String getEndTime() {return endTime;}public void setEndTime(String endTime) {this.endTime = endTime;}@Overridepublic String toString() {return "DictDataPageRequest{" +"dictCode=" + dictCode +", dictLabel='" + dictLabel + '\'' +", dictValue='" + dictValue + '\'' +", dictType='" + dictType + '\'' +", status='" + status + '\'' +", createBy='" + createBy + '\'' +", beginTime='" + beginTime + '\'' +", endTime='" + endTime + '\'' +", pageNum=" + getPageNum() +", pageSize=" + getPageSize() +", orderByColumn='" + getOrderByColumn() + '\'' +", isAsc='" + getIsAsc() + '\'' +'}';}
}
3.3 设计要点
- 继承关系:
extends PageObject implements Serializable
- 默认值设置: 在构造函数中设置合理的默认分页参数
- 字段注释: 明确标注每个字段的查询类型(精确/模糊/范围)
- toString方法: 包含所有字段,便于调试
4. Controller 层实现
4.1 分页查询接口实现
package com.hn.bitao.defined.report.module.web.controller;import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.hn.shengdun.base.PageResult;
import com.hn.shengdun.base.ResponseResult;
import com.hn.bitao.defined.report.module.pojo.DictData;
import com.hn.bitao.defined.report.module.dto.DictDataPageRequest;
import com.hn.bitao.defined.report.module.service.DictDataService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.validation.Valid;
import java.util.List;@RestController
@RequestMapping("/dict/data")
public class DictDataController {private static final Logger logger = LoggerFactory.getLogger(DictDataController.class);@Autowiredprivate DictDataService dictDataService;/*** 分页查询字典数据列表*/@PostMapping("/page")public ResponseResult<PageResult<DictData>> pageDictData(@Valid @RequestBody DictDataPageRequest pageRequest) {logger.info("分页查询字典数据列表,页码: {}, 页大小: {}", pageRequest.getPageNum(), pageRequest.getPageSize());// 将DTO转换为实体对象,只复制实体中存在的字段DictData dictData = new DictData();dictData.setDictLabel(pageRequest.getDictLabel());dictData.setDictValue(pageRequest.getDictValue());dictData.setDictType(pageRequest.getDictType());dictData.setStatus(pageRequest.getStatus());dictData.setCreateBy(pageRequest.getCreateBy());// beginTime和endTime是查询参数,不复制到实体对象中// 处理排序if (pageRequest.getOrderByColumn() != null && !pageRequest.getOrderByColumn().isEmpty()) {String orderBy = pageRequest.getOrderByColumn();if ("desc".equals(pageRequest.getIsAsc())) {orderBy += " desc";} else {orderBy += " asc";}PageHelper.orderBy(orderBy);}// 启动分页PageHelper.startPage(pageRequest.getPageNum(), pageRequest.getPageSize(), true);// 执行查询List<DictData> list = dictDataService.selectDictDataList(dictData);// 获取分页信息PageInfo<DictData> pageInfo = new PageInfo<>(list);// 返回分页结果return ResponseResult.successPage(list, pageInfo.getTotal());}
}
4.2 实现步骤详解
步骤1:参数接收和验证
@PostMapping("/page")
public ResponseResult<PageResult<DictData>> pageDictData(@Valid @RequestBody DictDataPageRequest pageRequest) {// @Valid 注解启用参数校验// @RequestBody 接收JSON格式的请求体
}
步骤2:DTO转实体对象
// 将DTO转换为实体对象,只复制实体中存在的字段
DictData dictData = new DictData();
dictData.setDictLabel(pageRequest.getDictLabel());
dictData.setDictValue(pageRequest.getDictValue());
dictData.setDictType(pageRequest.getDictType());
dictData.setStatus(pageRequest.getStatus());
dictData.setCreateBy(pageRequest.getCreateBy());
// beginTime和endTime是查询参数,不复制到实体对象中
步骤3:处理排序
// 处理排序
if (pageRequest.getOrderByColumn() != null && !pageRequest.getOrderByColumn().isEmpty()) {String orderBy = pageRequest.getOrderByColumn();if ("desc".equals(pageRequest.getIsAsc())) {orderBy += " desc";} else {orderBy += " asc";}PageHelper.orderBy(orderBy);
}
步骤4:启动分页
// 启动分页
PageHelper.startPage(pageRequest.getPageNum(), pageRequest.getPageSize(), true);
// 参数说明:
// pageRequest.getPageNum() - 页码
// pageRequest.getPageSize() - 页大小
// true - 是否进行count查询(获取总记录数)
步骤5:执行查询
// 执行查询
List<DictData> list = dictDataService.selectDictDataList(dictData);
// 注意:PageHelper.startPage() 之后的第一个查询会被自动分页
步骤6:获取分页信息
// 获取分页信息
PageInfo<DictData> pageInfo = new PageInfo<>(list);
// PageInfo 包含了分页的所有信息:总记录数、总页数、当前页等
步骤7:返回结果
// 返回分页结果
return ResponseResult.successPage(list, pageInfo.getTotal());
// ResponseResult.successPage() 是项目封装的分页响应方法
5. Service 层实现
5.1 Service 接口定义
package com.hn.bitao.defined.report.module.service;import com.hn.bitao.defined.report.module.pojo.DictData;
import java.util.List;/*** 字典数据服务接口*/
public interface DictDataService {/*** 查询字典数据列表* @param dictData 查询条件* @return 字典数据列表*/List<DictData> selectDictDataList(DictData dictData);
}
5.2 Service 实现类
package com.hn.bitao.defined.report.module.service.impl;import com.hn.bitao.defined.report.module.mapper.DictDataMapper;
import com.hn.bitao.defined.report.module.pojo.DictData;
import com.hn.bitao.defined.report.module.service.DictDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class DictDataServiceImpl implements DictDataService {@Autowiredprivate DictDataMapper dictDataMapper;@Overridepublic List<DictData> selectDictDataList(DictData dictData) {return dictDataMapper.selectDictDataList(dictData);}
}
5.3 Service 层要点
- 简单转发: Service层通常只是简单地调用Mapper层方法
- 业务逻辑: 如果有复杂的业务逻辑,在Service层处理
- 事务管理: 可以在Service层添加@Transactional注解
6. Mapper 层实现
6.1 Mapper 接口定义
package com.hn.bitao.defined.report.module.mapper;import com.hn.bitao.defined.report.module.pojo.DictData;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;/*** 字典数据Mapper接口*/
@Mapper
public interface DictDataMapper {/*** 查询字典数据列表* @param dictData 查询条件* @return 字典数据列表*/List<DictData> selectDictDataList(DictData dictData);
}
6.2 MyBatis XML 映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hn.bitao.defined.report.module.mapper.DictDataMapper"><!-- 字典数据结果映射 --><resultMap id="DictDataResult" type="DictData"><id property="dictCode" column="dict_code" /><result property="dictSort" column="dict_sort" /><result property="dictLabel" column="dict_label" /><result property="dictValue" column="dict_value" /><result property="dictType" column="dict_type" /><result property="cssClass" column="css_class" /><result property="listClass" column="list_class" /><result property="isDefault" column="is_default" /><result property="status" column="status" /><result property="createBy" column="create_by" /><result property="createTime" column="create_time" /><result property="updateBy" column="update_by" /><result property="updateTime" column="update_time" /><result property="remark" column="remark" /></resultMap><!-- 查询字典数据列表 --><select id="selectDictDataList" parameterType="DictData" resultMap="DictDataResult">SELECT dict_code, dict_sort, dict_label, dict_value, dict_type,css_class, list_class, is_default, status,create_by, create_time, update_by, update_time, remarkFROM sys_dict_data<where><if test="dictType != null and dictType != ''">AND dict_type LIKE CONCAT('%', #{dictType}, '%')</if><if test="dictLabel != null and dictLabel != ''">AND dict_label LIKE CONCAT('%', #{dictLabel}, '%')</if><if test="dictValue != null and dictValue != ''">AND dict_value LIKE CONCAT('%', #{dictValue}, '%')</if><if test="status != null and status != ''">AND status = #{status}</if><if test="createBy != null and createBy != ''">AND create_by LIKE CONCAT('%', #{createBy}, '%')</if></where>ORDER BY dict_sort ASC</select></mapper>
6.3 SQL 编写要点
- 动态SQL: 使用
<if>
标签实现条件查询 - 模糊查询: 使用
LIKE CONCAT('%', #{param}, '%')
- 精确查询: 直接使用
= #{param}
- 默认排序: 提供默认的排序规则
- 字段映射: 使用resultMap映射数据库字段到Java对象
7. 分页查询测试
7.1 基础分页查询
{"pageNum": 1,"pageSize": 10,"orderByColumn": "dict_sort","isAsc": "asc"
}
PowerShell测试命令:
$body = @{pageNum = 1pageSize = 10orderByColumn = "dict_sort"isAsc = "asc"
} | ConvertTo-JsonInvoke-RestMethod -Uri "http://localhost:8080/defined-report/dict/data/page" `-Method POST `-Body $body `-ContentType "application/json"
7.2 条件分页查询
{"pageNum": 1,"pageSize": 5,"dictType": "sys_user_sex","status": "0","orderByColumn": "create_time","isAsc": "desc"
}
PowerShell测试命令:
$body = @{pageNum = 1pageSize = 5dictType = "sys_user_sex"status = "0"orderByColumn = "create_time"isAsc = "desc"
} | ConvertTo-JsonInvoke-RestMethod -Uri "http://localhost:8080/defined-report/dict/data/page" `-Method POST `-Body $body `-ContentType "application/json"
7.3 预期响应格式
{"traceId": "C52FAAF7C3B64B8E9264AF7DE749DA6C","result": true,"code": 200,"msg": "","data": {"data": [{"dictCode": 1,"dictLabel": "男","dictValue": "0","dictType": "sys_user_sex","dictSort": 1,"status": "0","createTime": "2025-01-27 10:00:00"}],"count": 10},"success": true
}
8. 常见问题和解决方案
8.1 PageHelper 不生效
问题: 分页查询返回了所有数据,没有分页效果
原因:
- PageHelper.startPage() 没有紧跟查询语句
- PageHelper.startPage() 和查询之间有其他数据库操作
- PageHelper 配置不正确
解决方案:
// ✅ 正确写法
PageHelper.startPage(pageNum, pageSize);
List<DictData> list = dictDataService.selectDictDataList(dictData);// ❌ 错误写法
PageHelper.startPage(pageNum, pageSize);
// 中间有其他操作
int count = dictDataService.countData();
List<DictData> list = dictDataService.selectDictDataList(dictData);
8.2 排序不生效
问题: 设置了orderByColumn但排序不生效
原因:
- 排序字段名不正确
- PageHelper.orderBy() 调用位置不对
- SQL中有默认排序覆盖了PageHelper的排序
解决方案:
// ✅ 正确写法
if (pageRequest.getOrderByColumn() != null && !pageRequest.getOrderByColumn().isEmpty()) {String orderBy = pageRequest.getOrderByColumn();if ("desc".equals(pageRequest.getIsAsc())) {orderBy += " desc";} else {orderBy += " asc";}PageHelper.orderBy(orderBy);
}
PageHelper.startPage(pageRequest.getPageNum(), pageRequest.getPageSize(), true);
8.3 总记录数不准确
问题: 分页查询的总记录数count不正确
原因:
- PageHelper.startPage() 的第三个参数设置为false
- 查询条件在count查询时没有生效
解决方案:
// ✅ 正确写法 - 启用count查询
PageHelper.startPage(pageRequest.getPageNum(), pageRequest.getPageSize(), true);// 确保查询条件在Mapper的XML中正确处理
8.4 内存溢出
问题: 大数据量分页查询导致内存溢出
原因:
- pageSize设置过大
- 没有合理的分页限制
解决方案:
// 在Controller中添加分页参数校验
if (pageRequest.getPageSize() > 100) {pageRequest.setPageSize(100);
}
if (pageRequest.getPageNum() < 1) {pageRequest.setPageNum(1);
}
9. 最佳实践
9.1 分页参数校验
/*** 分页参数校验和默认值设置*/
public void validatePageParams(DictDataPageRequest pageRequest) {// 页码校验if (pageRequest.getPageNum() == null || pageRequest.getPageNum() < 1) {pageRequest.setPageNum(1);}// 页大小校验if (pageRequest.getPageSize() == null || pageRequest.getPageSize() < 1) {pageRequest.setPageSize(10);}// 页大小上限if (pageRequest.getPageSize() > 100) {pageRequest.setPageSize(100);}// 排序方向校验if (pageRequest.getIsAsc() != null && !"asc".equals(pageRequest.getIsAsc()) && !"desc".equals(pageRequest.getIsAsc())) {pageRequest.setIsAsc("desc");}
}
9.2 SQL注入防护
/*** 排序字段白名单校验*/
private static final Set<String> ALLOWED_ORDER_COLUMNS = Set.of("dict_code", "dict_sort", "dict_label", "dict_value", "dict_type", "status", "create_time", "update_time"
);public void validateOrderColumn(String orderByColumn) {if (orderByColumn != null && !ALLOWED_ORDER_COLUMNS.contains(orderByColumn)) {throw new IllegalArgumentException("不支持的排序字段: " + orderByColumn);}
}
9.3 性能优化
/*** 大数据量分页优化*/
@PostMapping("/page")
public ResponseResult<PageResult<DictData>> pageDictData(@Valid @RequestBody DictDataPageRequest pageRequest) {// 1. 参数校验validatePageParams(pageRequest);validateOrderColumn(pageRequest.getOrderByColumn());// 2. 缓存热点数据String cacheKey = generateCacheKey(pageRequest);PageResult<DictData> cachedResult = redisTemplate.opsForValue().get(cacheKey);if (cachedResult != null) {return ResponseResult.success(cachedResult);}// 3. 执行查询// ... 分页查询逻辑// 4. 缓存结果redisTemplate.opsForValue().set(cacheKey, result, Duration.ofMinutes(5));return ResponseResult.success(result);
}
9.4 日志记录
@PostMapping("/page")
public ResponseResult<PageResult<DictData>> pageDictData(@Valid @RequestBody DictDataPageRequest pageRequest) {long startTime = System.currentTimeMillis();logger.info("开始分页查询字典数据,参数: {}", pageRequest);try {// 分页查询逻辑PageResult<DictData> result = performPageQuery(pageRequest);long endTime = System.currentTimeMillis();logger.info("分页查询完成,耗时: {}ms, 返回记录数: {}, 总记录数: {}", endTime - startTime, result.getData().size(), result.getCount());return ResponseResult.success(result);} catch (Exception e) {long endTime = System.currentTimeMillis();logger.error("分页查询失败,耗时: {}ms, 参数: {}", endTime - startTime, pageRequest, e);throw e;}
}
10. 总结
10.1 分页查询核心要点
- 继承PageObject: 统一分页字段标准
- PageHelper使用: 正确的调用顺序和参数
- 参数校验: 防止非法参数和SQL注入
- 性能优化: 合理的分页大小和缓存策略
- 错误处理: 完善的异常处理和日志记录
10.2 开发流程
- 设计分页请求类: 继承PageObject,添加业务字段
- 实现Controller: 参数接收、排序处理、分页启动
- 实现Service: 业务逻辑处理(通常简单转发)
- 实现Mapper: 动态SQL查询,支持条件筛选
- 编写测试: 覆盖各种分页场景
- 性能调优: 监控查询性能,优化慢查询
10.3 注意事项
- PageHelper顺序: startPage()必须紧跟查询语句
- 排序安全: 使用白名单验证排序字段
- 分页限制: 设置合理的pageSize上限
- 缓存策略: 对热点数据进行缓存
- 监控告警: 监控分页查询的性能指标
文档维护人员: AI助手
最后更新时间: 2025年1月27日
文档状态: 完整版,包含完整的分页查询实现指南