一、核心概念

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 性能优化要点

  1. COUNT 查询优化

    // 禁用COUNT查询(当明确知道不需要总数时)
    Page<User> page = new Page<>(1, 10).setSearchCount(false);
    
  2. 大表分页优化

    // 使用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 分页查询规范

  1. 强制排序规则

    // 分页必须指定排序字段
    Page<User> page = new Page<>(1, 10)
        .addOrder(OrderItem.desc("create_time"));
    
  2. 合理分页大小

    // 根据业务场景设置最大分页大小
    int pageSize = Math.min(request.getSize(), MAX_PAGE_SIZE);
    
  3. 前端分页参数校验

    // 前端示例:限制最小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] 直接使用前端参数
大表导出 流式查询+分批处理 一次性查询全部数据

核心原则

  1. 分页必排序:避免深度分页时出现数据不一致
  2. 限制分页大小:防止恶意请求导致性能问题
  3. 优化COUNT查询:尤其关注大表性能
  4. 深度分页用游标:替代传统OFFSET方案
  5. 复杂关联先分页:避免JOIN导致分页偏移计算不准确