mohのAI正在绞尽脑汁想思路ING···
mohのAI摘要
mohのAI-Lite

CommonPageRequest.defaultPage() 分页原理详解

一、概述

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

三、核心源码解析

3.1 CommonPageRequest.java(完整源码)

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;

/**
* 通用分页请求
*
* @author xuyuxiang
* @date 2021/12/18 14:43
*/
@Slf4j
public class CommonPageRequest {

/** HTTP请求参数名:每页条数 */
private static final String PAGE_SIZE_PARAM_NAME = "size";

/** HTTP请求参数名:当前页码 */
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);
}

/**
* 核心分页方法
* @param orderItemList 排序条件列表(可为null)
* @return MyBatis-Plus Page对象
*/
public static <T> Page<T> defaultPage(List<OrderItem> orderItemList) {

int size = 20; // 默认每页20条

int page = 1; // 默认第1页

// ========== 1. 从HTTP请求中获取每页条数 ==========
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);
}
}

// ========== 2. 从HTTP请求中获取当前页码 ==========
String pageString = CommonServletUtil.getParamFromRequest(PAGE_PARAM_NAME);
if (ObjectUtil.isNotEmpty(pageString)) {
try {
page = Convert.toInt(pageString);
} catch (Exception e) {
log.error(">>> 分页页数转换异常:", e);
}
}

// ========== 3. 创建Page对象 ==========
Page<T> objectPage = new Page<>(page, size);

// ========== 4. 设置排序条件 ==========
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
# 获取第2页,每页10条
GET /api/project/attend/pageWithTeacherWork?current=2&size=10

# 只传current,使用默认size=20
GET /api/project/attend/pageWithTeacherWork?current=3

# 都不传,使用默认值 page=1, size=20
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
// 文件:snowy-plugin/.../service/impl/TrcProjectAttendRecordServiceImpl.java

@Override
public Page<TrcProjectAttendRecord> pageWithTeacherWork(TrcProjectAttendRecordPageParam param) {
// 使用MyBatis-Plus连表查询(LEFT JOIN TRC_TEACHER_WORK)
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
// 文件:snowy-plugin/.../mapper/TrcProjectAttendRecordMapper.java

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
<!-- 文件:TrcProjectAttendRecordMapper.xml -->

<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 关联教师作品表 -->
LEFT JOIN TRC_TEACHER_WORK t2
ON t1.TEACHER_WORK_ID = t2.ID AND t2.DELETE_FLAG = 'NOT_DELETE'
<!-- LEFT JOIN 关联获奖信息表 -->
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 关联评审记录表 -->
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'
<!-- 动态条件:项目ID -->
<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
-- 分页SQL
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

-- COUNT SQL(自动生成)
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 包含以下分页信息:
result.getCurrent(); // 当前页码:2
result.getSize(); // 每页条数:10
result.getTotal(); // 总记录数:85
result.getPages(); // 总页数:9
result.getRecords(); // 当前页的数据列表
result.hasNext(); // 是否有下一页:true
result.hasPrevious(); // 是否有上一页:true

八、设计优势

序号 优势 说明
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 请求传递 currentsize 参数
  • 不传参时使用默认值(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
// 1. 无排序的分页(最常用)
Page<T> page = CommonPageRequest.defaultPage();

// 2. 带排序的分页
List<OrderItem> orderItems = new ArrayList<>();
orderItems.add(OrderItem.desc("createTime"));
orderItems.add(OrderItem.asc("teacherName"));
Page<T> page = CommonPageRequest.defaultPage(orderItems);

// 3. 简单排序快捷方法
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 返回给前端