1. 分页查询基础概念与原生实现
在Web应用开发中,分页查询是最基础也最常用的功能之一。想象一下,当用户查询数据库中有上万条记录时,如果一次性全部返回给前端,不仅会造成网络传输压力,还会严重影响用户体验。这就好比在图书馆找书——管理员不会把整个图书馆的书都搬给你,而是根据你的需求每次拿几本给你查阅。
1.1 分页的核心要素
一个完整的分页功能需要包含以下核心参数:
- 当前页码(pageNo):用户请求的是第几页数据
- 每页数量(pageSize):每页展示多少条记录
- 总记录数(total):符合条件的数据总量
- 数据列表(list):当前页的实际数据集合
在MySQL等关系型数据库中,通常使用LIMIT offset, size语法实现分页,其中offset的计算公式为:
java复制int offset = (pageNo - 1) * pageSize;
1.2 原生分页实现详解
1.2.1 三层架构分工
在传统SpringBoot项目中,分页功能通常按照MVC三层架构实现:
Controller层:
java复制@PostMapping("/rooms")
public PageResult<Room> queryRooms(
@RequestParam(defaultValue = "1") int pageNo,
@RequestParam(defaultValue = "10") int pageSize,
@RequestBody RoomQuery query) {
return roomService.queryRooms(pageNo, pageSize, query);
}
Service层核心逻辑:
java复制public PageResult<Room> queryRooms(int pageNo, int pageSize, RoomQuery query) {
// 计算起始行
int offset = (pageNo - 1) * pageSize;
// 查询当前页数据
List<Room> list = roomMapper.selectByQuery(query, offset, pageSize);
// 查询总数
long total = roomMapper.countByQuery(query);
return new PageResult<>(list, total);
}
Mapper层SQL示例:
xml复制<select id="selectByQuery" resultType="Room">
SELECT * FROM room
<where>
<if test="query.name != null">
AND name LIKE CONCAT('%', #{query.name}, '%')
</if>
</where>
LIMIT #{offset}, #{pageSize}
</select>
<select id="countByQuery" resultType="long">
SELECT COUNT(*) FROM room
<where>
<if test="query.name != null">
AND name LIKE CONCAT('%', #{query.name}, '%')
</if>
</where>
</select>
1.2.2 原生实现的痛点
虽然这种方式能实现基本分页需求,但在实际项目中会暴露几个明显问题:
- 代码重复:每个分页方法都需要编写几乎相同的分页逻辑
- 维护困难:当需要修改分页逻辑时,需要改动所有相关方法
- 容易出错:手动计算offset容易出错,特别是复杂查询场景
- 功能单一:缺少对复杂分页需求的支持,如排序、多表关联等
经验之谈:在中小型项目中,如果分页需求简单且数量不多,原生实现尚可接受。但当项目规模扩大、分页需求复杂化后,这种方式的维护成本会呈指数级增长。
2. PageHelper插件深度解析
2.1 PageHelper核心原理
PageHelper是国内最流行的MyBatis分页插件,其核心原理可以概括为:
- 拦截器机制:通过实现MyBatis的Interceptor接口,在SQL执行前进行拦截
- 自动改写SQL:根据分页参数自动为原始SQL添加LIMIT子句
- 执行计数查询:自动生成并执行COUNT查询获取总记录数
- 结果封装:将分页结果封装到Page或PageInfo对象中
mermaid复制graph TD
A[调用PageHelper.startPage] --> B[设置分页参数到ThreadLocal]
B --> C[执行Mapper查询方法]
C --> D[拦截器检测到分页请求]
D --> E[改写原始SQL添加LIMIT]
D --> F[生成COUNT查询SQL]
E --> G[执行分页查询]
F --> H[执行COUNT查询]
G --> I[封装结果到Page对象]
H --> I
2.2 完整集成指南
2.2.1 依赖配置
在pom.xml中添加最新版PageHelper Starter:
xml复制<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
SpringBoot配置示例(application.yml):
yaml复制pagehelper:
helper-dialect: mysql # 数据库方言
reasonable: true # 分页合理化
support-methods-arguments: true # 支持接口参数
params: count=countSql # COUNT查询参数名
2.2.2 基础使用模式
基本查询模式:
java复制// 开启分页
PageHelper.startPage(pageNo, pageSize);
// 紧接着的查询会被分页
List<User> users = userMapper.selectByExample(example);
// 用PageInfo包装结果
PageInfo<User> pageInfo = new PageInfo<>(users);
带条件查询示例:
java复制public PageInfo<Order> queryOrders(OrderQuery query, int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
Example example = new Example(Order.class);
if (StringUtils.isNotBlank(query.getOrderNo())) {
example.createCriteria().andLike("orderNo", "%" + query.getOrderNo() + "%");
}
example.setOrderByClause("create_time DESC");
List<Order> orders = orderMapper.selectByExample(example);
return new PageInfo<>(orders);
}
2.3 高级特性详解
2.3.1 分页参数传递方式
PageHelper支持多种参数传递方式:
- 方法参数方式:
java复制@GetMapping("/users")
public PageInfo<User> getUsers(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
PageHelper.startPage(pageNum, pageSize);
return new PageInfo<>(userMapper.selectAll());
}
- 实体类封装方式:
java复制public class PageParam {
private Integer pageNum = 1;
private Integer pageSize = 10;
// getters & setters
}
public PageInfo<User> getUsers(PageParam param) {
PageHelper.startPage(param.getPageNum(), param.getPageSize());
return new PageInfo<>(userMapper.selectAll());
}
- ThreadLocal方式:
java复制PageHelper.startPage(1, 10);
try {
// 业务代码
} finally {
PageHelper.clearPage(); // 必须清理
}
2.3.2 复杂查询支持
多表关联查询:
java复制public PageInfo<OrderVO> queryOrderDetail(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<OrderVO> list = orderMapper.selectWithDetail();
return new PageInfo<>(list);
}
// Mapper接口
@Select("SELECT o.*, u.username FROM orders o LEFT JOIN user u ON o.user_id=u.id")
List<OrderVO> selectWithDetail();
自定义COUNT查询:
xml复制<select id="selectComplex" resultMap="BaseResultMap">
SELECT * FROM table WHERE ...
</select>
<select id="selectComplex_COUNT" resultType="Long">
SELECT COUNT(*) FROM table WHERE ...
</select>
3. Page与PageInfo核心对比
3.1 Page接口分析
Page是分页结果的原始接口,主要提供:
- 分页数据:通过getResult()获取
- 基础分页信息:当前页、每页数量、总页数等
java复制public interface Page<E> extends List<E> {
int getPageNum(); // 当前页码
int getPageSize(); // 每页数量
int getPages(); // 总页数
long getTotal(); // 总记录数
List<E> getResult(); // 当前页数据
// 其他方法...
}
3.2 PageInfo完整功能
PageInfo是更强大的分页包装类,额外提供:
- 导航页码:可自定义显示的页码数量
- 边界判断:是否第一页/最后一页等
- 完整分页信息:包含所有分页相关属性
java复制// 典型使用场景
PageInfo<User> pageInfo = new PageInfo<>(userList);
model.addAttribute("pageInfo", pageInfo);
// 前端可获取的完整属性
pageInfo.getPageNum(); // 当前页
pageInfo.getPageSize(); // 每页数量
pageInfo.getTotal(); // 总记录数
pageInfo.getPages(); // 总页数
pageInfo.getList(); // 当前页数据
pageInfo.isIsFirstPage(); // 是否第一页
pageInfo.isIsLastPage(); // 是否最后一页
pageInfo.getNavigatepageNums(); // 所有导航页码
3.3 性能优化建议
- 合理设置pageSize:避免单次查询数据量过大,建议控制在100条以内
- COUNT查询优化:对于复杂查询,考虑使用缓存或冗余字段
- 延迟加载:大数据量时考虑使用游标分页
- 索引优化:确保分页查询字段有合适索引
java复制// 大数据量分页优化示例
public PageInfo<Log> queryLargeData(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize, false); // 不执行COUNT查询
List<Log> logs = logMapper.selectLargeData();
// 手动执行COUNT查询
Page<Log> page = (Page<Log>) logs;
if (pageNum == 1 && page.size() < pageSize) {
page.setTotal(page.size());
} else {
page.setTotal(logMapper.countLargeData());
}
return new PageInfo<>(page);
}
4. 实战问题排查与解决方案
4.1 常见问题汇总
-
分页失效:
- 原因:PageHelper.startPage()与查询语句之间有其他数据库操作
- 解决:确保startPage()后紧跟需要分页的查询
-
总数不准确:
- 原因:使用了嵌套查询或复杂SQL
- 解决:自定义COUNT查询或使用PageHelper的count查询优化
-
内存溢出:
- 原因:查询结果集过大
- 解决:合理设置pageSize,或使用流式查询
-
排序失效:
- 原因:PageHelper的排序参数与SQL中ORDER BY冲突
- 解决:统一使用一种排序方式
4.2 性能优化案例
案例:千万级数据分页优化
原生分页在数据量大时会出现性能问题:
sql复制SELECT * FROM large_table LIMIT 1000000, 10
优化方案1 - 使用索引覆盖:
sql复制SELECT * FROM large_table
WHERE id >= (SELECT id FROM large_table ORDER BY id LIMIT 1000000, 1)
LIMIT 10
优化方案2 - 使用游标分页:
java复制public List<User> getUsersByCursor(Long lastId, int limit) {
return userMapper.selectAfterId(lastId, limit);
}
4.3 特殊场景处理
Map结果集分页:
java复制PageHelper.startPage(pageNum, pageSize);
List<Map<String, Object>> mapList = userMapper.selectAsMap();
PageInfo<Map<String, Object>> pageInfo = new PageInfo<>(mapList);
一对多结果分页:
java复制@Select("SELECT u.*, a.* FROM user u LEFT JOIN address a ON u.id=a.user_id")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "addresses", column = "id",
many = @Many(select = "selectAddressByUserId"))
})
List<User> selectUsersWithAddress();
// 分页时需要在主查询上分页
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectUsersWithAddress();
5. 最佳实践与扩展思考
5.1 项目中的分层封装
建议在项目中统一封装分页逻辑:
基础分页参数类:
java复制public class PageParam {
private Integer pageNum = 1;
private Integer pageSize = 10;
private String orderBy;
// 转换为PageHelper参数
public <E> Page<E> toPage() {
Page<E> page = PageHelper.startPage(pageNum, pageSize);
if (StringUtils.isNotBlank(orderBy)) {
PageHelper.orderBy(orderBy);
}
return page;
}
}
统一返回结构:
java复制public class PageResult<T> {
private Integer pageNum;
private Integer pageSize;
private Long total;
private Integer pages;
private List<T> list;
public static <T> PageResult<T> of(PageInfo<T> pageInfo) {
PageResult<T> result = new PageResult<>();
result.setPageNum(pageInfo.getPageNum());
result.setPageSize(pageInfo.getPageSize());
result.setTotal(pageInfo.getTotal());
result.setPages(pageInfo.getPages());
result.setList(pageInfo.getList());
return result;
}
}
5.2 前端对接规范
推荐的前端分页参数格式:
json复制{
"pageNum": 1,
"pageSize": 10,
"orderBy": "create_time desc",
"params": {
"name": "张",
"status": 1
}
}
统一返回格式:
json复制{
"code": 200,
"data": {
"list": [...],
"pageNum": 1,
"pageSize": 10,
"total": 100,
"pages": 10
}
}
5.3 扩展思考方向
- 分布式环境分页:如何保证多节点数据分页的一致性
- 弹性分页:根据系统负载动态调整pageSize
- 分页缓存策略:高频访问分页数据的缓存方案
- 安全分页:防止分页参数被恶意攻击
在微服务架构下,分页查询还需要考虑:
- 跨服务分页聚合
- 分页结果缓存
- 分页流量控制
java复制// 分布式分页示例
public PageResult<User> distributedQuery(PageParam param) {
// 从多个服务获取数据
List<User> part1 = userService.query(param);
List<User> part2 = orderService.queryUsers(param);
// 内存分页
List<User> combined = Stream.concat(part1.stream(), part2.stream())
.sorted(Comparator.comparing(User::getCreateTime).reversed())
.skip((param.getPageNum() - 1) * param.getPageSize())
.limit(param.getPageSize())
.collect(Collectors.toList());
// 获取总数需要特殊处理
long total = userService.count() + orderService.countUsers();
return new PageResult<>(param.getPageNum(), param.getPageSize(), total, combined);
}
在实际项目中使用PageHelper时,我发现合理配置和统一规范比技术实现更重要。建议团队制定分页开发规范,包括参数命名、返回结构、异常处理等,这能显著提高代码可维护性。对于特别复杂的查询场景,有时候回归原生分页反而更可控,不要为了使用插件而增加不必要的复杂度。