一、概述
CommonPageRequest.defaultPage() 是 Snowy 框架中封装的一个分页工具方法,用于从 HTTP 请求中提取分页参数,并创建 MyBatis-Plus 的 Page 对象。该方法采用插件拦截机制实现 SQL 自动改写,无需手动拼接 LIMIT 语句。
二、核心源码位置
| 文件 |
路径 |
| 分页请求工具类 |
snowy-common/src/main/java/vip/xiaonuo/common/page/CommonPageRequest.java |
| 分页插件配置 |
MyBatis-Plus 内置 PaginationInnerInterceptor |
| 业务Service实现 |
snowy-plugin/snowy-plugin-trc/src/main/java/vip/xiaonuo/trc/modular/projectattendrecord/service/impl/TrcProjectAttendRecordServiceImpl.java |
| Mapper XML |
snowy-plugin/snowy-plugin-trc/src/main/java/vip/xiaonuo/trc/modular/projectattendrecord/mapper/mapping/TrcProjectAttendRecordMapper.xml |
三、核心源码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| package vip.xiaonuo.common.page;
import cn.hutool.core.convert.Convert; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.extern.slf4j.Slf4j; import vip.xiaonuo.common.util.CommonServletUtil;
import java.util.List;
@Slf4j public class CommonPageRequest {
private static final String PAGE_SIZE_PARAM_NAME = "size";
private static final String PAGE_PARAM_NAME = "current";
private static final Integer PAGE_SIZE_MAX_VALUE = 100;
public static <T> Page<T> defaultPage() { return defaultPage(null); }
public static <T> Page<T> defaultPage(List<OrderItem> orderItemList) {
int size = 20;
int page = 1;
String pageSizeString = CommonServletUtil.getParamFromRequest(PAGE_SIZE_PARAM_NAME); if (ObjectUtil.isNotEmpty(pageSizeString)) { try { size = Convert.toInt(pageSizeString); if(size > PAGE_SIZE_MAX_VALUE) { size = PAGE_SIZE_MAX_VALUE; } } catch (Exception e) { log.error(">>> 分页条数转换异常:", e); } }
String pageString = CommonServletUtil.getParamFromRequest(PAGE_PARAM_NAME); if (ObjectUtil.isNotEmpty(pageString)) { try { page = Convert.toInt(pageString); } catch (Exception e) { log.error(">>> 分页页数转换异常:", e); } }
Page<T> objectPage = new Page<>(page, size);
if (ObjectUtil.isNotEmpty(orderItemList)) { objectPage.setOrders(orderItemList); } return objectPage; } }
|
3.2 代码逐行解析
| 行号 |
关键代码 |
说明 |
| 33 |
PAGE_SIZE_PARAM_NAME = "size" |
HTTP请求中每页条数的参数名 |
| 35 |
PAGE_PARAM_NAME = "current" |
HTTP请求中当前页码的参数名 |
| 37 |
PAGE_SIZE_MAX_VALUE = 100 |
每页条数的安全上限 |
| 45 |
int size = 20 |
默认每页20条 |
| 47 |
int page = 1 |
默认第1页 |
| 50 |
CommonServletUtil.getParamFromRequest() |
从HTTP请求中获取参数值 |
| 54-56 |
if(size > PAGE_SIZE_MAX_VALUE) |
超过100条时强制限制为100 |
| 71 |
new Page<>(page, size) |
创建MyBatis-Plus的Page对象 |
四、工作流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| ┌─────────────────────────────────────────────────────────────────┐ │ HTTP 请求入口 │ │ GET /api/xxx?current=2&size=10 │ └─────────────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ CommonServletUtil.getParamFromRequest() │ │ 从请求参数中提取分页信息 │ └─────────────────────────────┬───────────────────────────────────┘ │ ┌───────────────┴───────────────┐ ▼ ▼ ┌────────────────┐ ┌────────────────┐ │ 获取 "size" │ │ 获取 "current" │ │ 参数值 │ │ 参数值 │ └───────┬────────┘ └───────┬────────┘ │ │ ▼ ▼ ┌────────────────┐ ┌────────────────┐ │ 为空 → 默认20 │ │ 为空 → 默认1 │ │ 超过100 → 限制100│ │ │ └───────┬────────┘ └───────┬────────┘ │ │ └───────────────┬───────────────┘ ▼ ┌────────────────────────────┐ │ new Page<>(page, size) │ │ 创建MyBatis-Plus Page对象 │ └─────────────┬──────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 传入 Mapper 层方法 │ │ this.baseMapper.pageWithTeacherWork(page, param) │ └─────────────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ MyBatis-Plus PaginationInnerInterceptor │ │ SQL 拦截插件(自动拦截) │ └─────────────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ SQL 自动改写 │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 原始SQL: SELECT * FROM table LEFT JOIN ... │ │ │ │ │ │ │ │ 改写后SQL: SELECT * FROM table LEFT JOIN ... │ │ │ │ LIMIT 10 OFFSET 10 │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 数据库执行分页SQL │ │ 返回当前页数据 + 自动计算 total、pages 等信息 │ └─────────────────────────────────────────────────────────────────┘
|
五、参数提取说明
| 参数名 |
HTTP参数 |
默认值 |
最大值 |
数据类型 |
说明 |
| current |
current |
1 |
无限制 |
int |
当前页码,从1开始 |
| size |
size |
20 |
100 |
int |
每页条数 |
请求示例:
1 2 3 4 5 6 7 8
| GET /api/project/attend/pageWithTeacherWork?current=2&size=10
GET /api/project/attend/pageWithTeacherWork?current=3
GET /api/project/attend/pageWithTeacherWork
|
六、业务代码使用示例
6.1 Service 层调用(TrcProjectAttendRecordServiceImpl.java)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
@Override public Page<TrcProjectAttendRecord> pageWithTeacherWork(TrcProjectAttendRecordPageParam param) { Page<TrcProjectAttendRecord> page = CommonPageRequest.defaultPage(); Page<TrcProjectAttendRecord> trcProjectAttendRecordPage = this.baseMapper.pageWithTeacherWork(page, param);
for(TrcProjectAttendRecord trcProjectAttendRecord : trcProjectAttendRecordPage.getRecords()){ String teacherWorkId = trcProjectAttendRecord.getTeacherWorkId(); List<TrcTeacherWorkAttachment> trcTeacherWorkAttachmentList = trcTeacherWorkAttachmentService.list( new LambdaQueryWrapper<TrcTeacherWorkAttachment>() .eq(TrcTeacherWorkAttachment::getTeacherWorkId, teacherWorkId) .eq(TrcTeacherWorkAttachment::getDeleteFlag, "NOT_DELETE") ); trcProjectAttendRecord.setTrcProjectAttendRecordList(trcTeacherWorkAttachmentList); }
return trcProjectAttendRecordPage; }
|
6.2 Mapper 接口定义
1 2 3 4 5 6 7 8 9 10 11 12
|
public interface TrcProjectAttendRecordMapper extends BaseMapper<TrcProjectAttendRecord> {
Page<TrcProjectAttendRecord> pageWithTeacherWork( Page<TrcProjectAttendRecord> page, TrcProjectAttendRecordPageParam param ); }
|
6.3 Mapper XML 实现(核心连表查询)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
|
<select id="pageWithTeacherAward" resultMap="AwardResultMap"> SELECT t1.ID, t1.PROJECT_ID, t1.ENROLL_TIME, t1.WORK_UPLOAD_TIME, t1.WORK_REVIEW_TIME, t1.TEACHER_ID, t1.TEACHER_NAME, t1.SCHOOL_ID, t1.SCHOOL_NAME, t1.TEACHER_WORK_ID, t1.ENROLL_AUDIT, t1.INITIAL_AUDIT, t1.REVIEW_CONFIRM_AUDIT, t2.GRADE_LEVEL_NAME, t2.SUBJECT_NAME, t2.GRADE_NAME, t2.WORK_NAME, t2.WORK_DESCRIPTION, t2.ITEM_TYPE_NAME, t2.WORK_COVER_URL, t3.AWARD_LEVEL, t4.WORK_REVIEW_SCORE FROM TRC_PROJECT_ATTEND_RECORD t1 LEFT JOIN TRC_TEACHER_WORK t2 ON t1.TEACHER_WORK_ID = t2.ID AND t2.DELETE_FLAG = 'NOT_DELETE' LEFT JOIN TRC_TEACHER_AWARD t3 ON t1.ID = t3.PROJECT_ATTEND_RECORD_ID AND t1.TEACHER_WORK_ID = t3.TEACHER_WORK_ID AND t3.DELETE_FLAG = 'NOT_DELETE' LEFT JOIN TRC_PROJECT_ATTEND_REVIEW_RECORD t4 ON t1.ID = t4.PROJECT_ATTEND_RECORD_ID AND t4.DELETE_FLAG = 'NOT_DELETE' <where> t1.DELETE_FLAG = 'NOT_DELETE' <if test="param.projectId != null and param.projectId != ''"> AND t1.PROJECT_ID = #{param.projectId} </if> <if test="param.teacherName != null and param.teacherName != ''"> AND t1.TEACHER_NAME LIKE CONCAT('%', #{param.teacherName}, '%') </if> <if test="param.workName != null and param.workName != ''"> AND t2.WORK_NAME LIKE CONCAT('%', #{param.workName}, '%') </if> <if test="param.enrollAudit != null and param.enrollAudit != ''"> AND t1.ENROLL_AUDIT = #{param.enrollAudit} </if> </where> ORDER BY t1.CREATE_TIME DESC </select>
|
七、SQL 自动拦截改写机制
7.1 MyBatis-Plus 分页插件原理
MyBatis-Plus 通过 PaginationInnerInterceptor 拦截器实现自动分页:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| ┌──────────────────────────────────────────────────────────────┐ │ 执行流程 │ ├──────────────────────────────────────────────────────────────┤ │ │ │ 1. Executor.query() 执行查询 │ │ │ │ │ ▼ │ │ 2. PaginationInnerInterceptor 拦截 │ │ │ │ │ ├── 判断是否为Select查询 │ │ ├── 判断是否传入了Page对象 │ │ │ │ │ ▼ │ │ 3. 拦截并改写SQL │ │ │ │ │ ├── 执行 COUNT 查询(计算总数) │ │ ├── 改写原SQL添加 LIMIT 和 OFFSET │ │ │ │ │ ▼ │ │ 4. 执行改写后的分页SQL │ │ │ │ │ ▼ │ │ 5. 回填 Page 对象(total, pages, records) │ │ │ └──────────────────────────────────────────────────────────────┘
|
7.2 SQL 改写示例
请求:?current=2&size=10(查询第2页,每页10条)
原始 SQL:
1 2 3 4 5
| SELECT t1.ID, t1.TEACHER_NAME, t2.WORK_NAME FROM TRC_PROJECT_ATTEND_RECORD t1 LEFT JOIN TRC_TEACHER_WORK t2 ON t1.TEACHER_WORK_ID = t2.ID WHERE t1.DELETE_FLAG = 'NOT_DELETE' ORDER BY t1.CREATE_TIME DESC
|
自动改写后的 SQL:
1 2 3 4 5 6 7 8 9 10 11 12
| SELECT t1.ID, t1.TEACHER_NAME, t2.WORK_NAME FROM TRC_PROJECT_ATTEND_RECORD t1 LEFT JOIN TRC_TEACHER_WORK t2 ON t1.TEACHER_WORK_ID = t2.ID WHERE t1.DELETE_FLAG = 'NOT_DELETE' ORDER BY t1.CREATE_TIME DESC LIMIT 10 OFFSET 10
SELECT COUNT(*) FROM TRC_PROJECT_ATTEND_RECORD t1 LEFT JOIN TRC_TEACHER_WORK t2 ON t1.TEACHER_WORK_ID = t2.ID WHERE t1.DELETE_FLAG = 'NOT_DELETE'
|
7.3 Page 对象返回信息
1 2 3 4 5 6 7 8 9 10
| Page<TrcProjectAttendRecord> result = this.baseMapper.pageWithTeacherWork(page, param);
result.getCurrent(); result.getSize(); result.getTotal(); result.getPages(); result.getRecords(); result.hasNext(); result.hasPrevious();
|
八、设计优势
| 序号 |
优势 |
说明 |
| 1 |
零配置 |
无需手动设置分页参数,自动从HTTP请求中提取 |
| 2 |
安全限制 |
每页最大100条,防止一次查询过多数据导致内存溢出 |
| 3 |
统一入口 |
所有分页查询统一使用该方法,保持代码一致性 |
| 4 |
数据库适配 |
自动适配MySQL、PostgreSQL等多种数据库方言 |
| 5 |
排序支持 |
可传入排序条件,支持多字段排序 |
| 6 |
异常处理 |
参数转换异常时自动降级使用默认值 |
| 7 |
性能优化 |
仅查询当前页数据,避免全表扫描返回大量数据 |
九、关键依赖
| 依赖 |
作用 |
来源 |
Page<T> |
分页对象封装 |
MyBatis-Plus |
PaginationInnerInterceptor |
SQL拦截改写 |
MyBatis-Plus |
CommonServletUtil |
HTTP参数提取 |
Snowy框架 |
cn.hutool.core.convert.Convert |
类型转换 |
Hutool工具库 |
cn.hutool.core.util.ObjectUtil |
对象判空 |
Hutool工具库 |
十、注意事项
10.1 分页参数来源
- 必须通过 HTTP 请求传递
current 和 size 参数
- 不传参时使用默认值(page=1, size=20)
- 参数值为空字符串时使用默认值
10.2 连表查询分页
- LEFT JOIN 时分页在主表(LEFT JOIN 左侧)上进行
- 需确保关联条件正确,否则可能出现数据丢失或重复
- 建议在关联条件中加上
DELETE_FLAG = 'NOT_DELETE' 软删除条件
10.3 性能考虑
- 深分页(页码较大,如第1000页)时性能较差,建议使用游标分页
- 大数据量场景建议限制每页条数或使用条件分页
- COUNT查询在数据量大时也会有性能损耗
10.4 常见问题
| 问题 |
原因 |
解决方案 |
| 分页数据错乱 |
排序条件不唯一 |
确保有唯一字段排序(如ID) |
| COUNT很慢 |
关联表太多 |
优化SQL或使用缓存 |
| 总数为0 |
WHERE条件过滤掉了所有数据 |
检查查询条件 |
十一、其他分页方法变体
1 2 3 4 5 6 7 8 9 10 11
| Page<T> page = CommonPageRequest.defaultPage();
List<OrderItem> orderItems = new ArrayList<>(); orderItems.add(OrderItem.desc("createTime")); orderItems.add(OrderItem.asc("teacherName")); Page<T> page = CommonPageRequest.defaultPage(orderItems);
Page<T> page = CommonPageRequest.defaultPage(OrderItem.desc("id"));
|
十二、完整调用链路总结
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 前端请求 │ ▼ Controller 接收请求 │ ▼ Service.pageWithTeacherWork(param) │ ├── CommonPageRequest.defaultPage() ← 从HTTP请求提取分页参数 │ │ │ └── CommonServletUtil.getParamFromRequest("current/size") │ │ │ └── 构造 new Page<>(page, size) │ ├── baseMapper.pageWithTeacherWork(page, param) ← 传入Mapper │ │ │ └── Mapper XML SQL执行 │ │ │ └── PaginationInnerInterceptor 拦截 │ │ │ ├── COUNT查询 → total │ └── LIMIT/OFFSET → 分页数据 │ └── 返回 Page 对象(含records、total、pages等) │ ▼ Controller 返回给前端
|