一、核心概念
1.1 Page 对象的作用
- 分页信息容器:封装当前页码、每页数量、总记录数、总页数等核心分页数据
- 查询参数载体:作为分页查询方法的参数传入
- 结果集容器:存储查询结果的分页数据
1.2 分页插件工作原理
sequenceDiagram participant App as 应用程序 participant Interceptor as 分页插件 participant Executor as MyBatis 执行器 participant DB as 数据库 App->>Interceptor: 创建 Page 对象并执行查询 Interceptor->>Executor: 拦截原始 SQL Interceptor->>DB: 1. 执行 COUNT 查询获取总数 DB-->>Interceptor: 返回总数 Interceptor->>DB: 2. 执行分页 SQL (添加 LIMIT/OFFSET) DB-->>Interceptor: 返回分页数据 Interceptor->>App: 组装 Page 对象返回结果
1.3 核心属性说明
属性 | 类型 | 说明 | 示例值 |
---|---|---|---|
records |
List |
当前页数据列表 | [User1, User2] |
total |
long | 总记录数 | 125 |
size |
long | 每页显示条数 | 10 |
current |
long | 当前页码 | 1 |
pages |
long | 总页数(自动计算) | 13 |
二、详细操作步骤
2.1 配置分页插件(Spring Boot)
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
2.2 基础分页查询
// 创建分页参数(第1页,每页10条)
Page<User> page = new Page<>(1, 10);
// 执行分页查询
Page<User> result = userMapper.selectPage(page, null);
// 获取分页数据
List<User> users = result.getRecords(); // 当前页数据
long total = result.getTotal(); // 总记录数
long pages = result.getPages(); // 总页数
2.3 带条件的分页查询
// 创建分页参数(第2页,每页5条)
Page<User> page = new Page<>(2, 5);
// 创建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 1)
.like(User::getName, "张")
.orderByDesc(User::getCreateTime);
// 执行带条件的分页查询
Page<User> result = userMapper.selectPage(page, wrapper);
2.4 自定义分页查询(XML/注解)
Mapper 接口:
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 自定义分页查询方法
Page<User> selectUserPage(@Param("page") Page<User> page,
@Param("name") String name);
}
XML 映射文件:
<select id="selectUserPage" resultType="User">
SELECT id, name, age, email
FROM user
WHERE name LIKE CONCAT(#{name}, '%')
<!-- 无需写LIMIT,插件自动处理 -->
</select>
调用方式:
Page<User> page = new Page<>(1, 10);
Page<User> result = userMapper.selectUserPage(page, "张");
2.5 分页结果转换
Page<User> userPage = ... // 获取分页数据
// 转换为自定义DTO分页
Page<UserDTO> dtoPage = userPage.convert(user -> {
UserDTO dto = new UserDTO();
dto.setUserId(user.getId());
dto.setUserName(user.getName());
dto.setUserAge(user.getAge());
return dto;
});
三、常见错误与解决
3.1 未配置分页插件
// ❌ 错误现象:
// 调用selectPage返回所有记录,没有分页效果
// ✅ 解决方案:
// 确保已正确配置PaginationInnerInterceptor
3.2 错误的分页参数
// ❌ 错误用法:
Page<User> page = new Page<>();
page.setCurrent(0); // 页码从0开始
page.setSize(10000); // 单页过大
// ✅ 正确做法:
// 页码从1开始,合理控制分页大小
Page<User> page = new Page<>(1, 50);
3.3 自定义SQL未正确处理分页
<!-- ❌ 错误:手动添加了LIMIT -->
<select id="selectUserPage" resultType="User">
SELECT * FROM user
LIMIT #{page.offset}, #{page.size} <!-- 导致重复分页 -->
</select>
<!-- ✅ 正确:交给插件处理 -->
<select id="selectUserPage" resultType="User">
SELECT * FROM user <!-- 插件自动添加LIMIT -->
</select>
四、关键注意事项
4.1 性能优化要点
COUNT 查询优化:
// 禁用COUNT查询(当明确知道不需要总数时) Page<User> page = new Page<>(1, 10).setSearchCount(false);
大表分页优化:
// 使用id范围分页替代传统LIMIT wrapper.gt(User::getId, lastId).last("LIMIT 10");
4.2 安全防护
// 防止恶意超大分页请求
int maxPageSize = 100;
int pageSize = Math.min(request.getSize(), maxPageSize);
Page<User> page = new Page<>(request.getPage(), pageSize);
4.3 多数据源适配
// 根据数据源设置数据库类型
new PaginationInnerInterceptor(DbType.MYSQL); // MySQL
new PaginationInnerInterceptor(DbType.ORACLE); // Oracle
new PaginationInnerInterceptor(DbType.POSTGRE_SQL); // PostgreSQL
五、高级使用技巧
5.1 分页参数自动注入
Controller 层简化:
@GetMapping("/users")
public Page<User> getUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return userService.page(new Page<>(page, size));
}
5.2 自定义 COUNT 查询
<!-- 优化COUNT查询 -->
<select id="selectUserPage_count" resultType="long">
SELECT COUNT(1) FROM user WHERE name LIKE CONCAT(#{name}, '%')
</select>
Mapper 接口:
Page<User> selectUserPage(@Param("page") Page<User> page,
@Param("name") String name);
5.3 一对多分页处理
// 主查询分页
Page<Order> orderPage = orderMapper.selectPage(page, wrapper);
// 批量获取关联数据
List<Long> orderIds = orderPage.getRecords().stream()
.map(Order::getId)
.collect(Collectors.toList());
// 查询订单详情
Map<Long, List<OrderItem>> itemMap = orderItemService
.listByOrderIds(orderIds)
.stream()
.collect(Collectors.groupingBy(OrderItem::getOrderId));
// 组装数据
orderPage.getRecords().forEach(order ->
order.setItems(itemMap.get(order.getId())));
六、最佳实践与性能优化
6.1 分页性能优化策略
场景 | 优化方案 | 效果 |
---|---|---|
大表深度分页 | 基于游标的分页(WHERE id > last_id) | 避免OFFSET性能问题 |
复杂COUNT查询 | 覆盖索引/单独优化COUNT SQL | 减少全表扫描 |
多表关联分页 | 先分页主表再查关联 | 避免JOIN分页偏移计算 |
海量数据导出 | 流式查询+分批处理 | 避免OOM |
6.2 分页查询规范
强制排序规则:
// 分页必须指定排序字段 Page<User> page = new Page<>(1, 10) .addOrder(OrderItem.desc("create_time"));
合理分页大小:
// 根据业务场景设置最大分页大小 int pageSize = Math.min(request.getSize(), MAX_PAGE_SIZE);
前端分页参数校验:
// 前端示例:限制最小1页,每页10-100条 const page = Math.max(1, currentPage); const size = Math.min(Math.max(10, pageSize), 100);
6.3 百万级数据分页方案
// 基于游标的深度分页
Long lastId = 0L; // 起始ID
int pageSize = 100;
while (true) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.gt(User::getId, lastId)
.orderByAsc(User::getId)
.last("LIMIT " + pageSize);
List<User> users = userMapper.selectList(wrapper);
if (users.isEmpty()) break;
// 处理业务逻辑
processBatch(users);
// 更新最后ID
lastId = users.get(users.size() - 1).getId();
}
七、完整示例
7.1 完整分页查询服务
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 最大分页大小
private static final int MAX_PAGE_SIZE = 100;
public Page<UserDTO> searchUsers(UserQuery query) {
// 参数校验
int pageNum = Math.max(1, query.getPage());
int pageSize = Math.min(Math.max(10, query.getSize()), MAX_PAGE_SIZE);
// 创建分页对象(强制按创建时间排序)
Page<User> page = new Page<>(pageNum, pageSize)
.addOrder(OrderItem.desc("create_time"));
// 构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.isNotBlank(query.getDept()),
User::getDepartment, query.getDept())
.like(StringUtils.isNotBlank(query.getName()),
User::getName, query.getName() + "%");
// 执行分页查询
Page<User> userPage = userMapper.selectPage(page, wrapper);
// 转换为DTO
return userPage.convert(this::toDTO);
}
private UserDTO toDTO(User user) {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setName(user.getName());
dto.setEmail(user.getEmail());
dto.setCreateTime(user.getCreateTime());
return dto;
}
}
7.2 分页接口返回格式
{
"code": 0,
"msg": "success",
"data": {
"records": [
{"id": 1, "name": "张三", "email": "zhangsan@example.com"},
{"id": 2, "name": "李四", "email": "lisi@example.com"}
],
"total": 125,
"size": 10,
"current": 1,
"pages": 13
}
}
总结:分页查询最佳实践矩阵
实践要点 | 推荐做法 | 避免做法 |
---|---|---|
分页初始化 | new Page<>(current, size).addOrder(...) |
new Page<>() 不设置排序 |
分页大小控制 | 限制最大 pageSize (如100) | 允许前端任意设置 size |
深度分页 | 使用基于ID的范围查询 | LIMIT 100000, 10 |
COUNT优化 | 覆盖索引/简化COUNT条件 | 复杂JOIN的COUNT |
一对多分页 | 先分页主表再批量查关联 | 直接JOIN分页 |
参数校验 | 服务端校验 page≥1, size∈[10,100] | 直接使用前端参数 |
大表导出 | 流式查询+分批处理 | 一次性查询全部数据 |
核心原则:
- 分页必排序:避免深度分页时出现数据不一致
- 限制分页大小:防止恶意请求导致性能问题
- 优化COUNT查询:尤其关注大表性能
- 深度分页用游标:替代传统OFFSET方案
- 复杂关联先分页:避免JOIN导致分页偏移计算不准确