1. 为什么需要分页查询?
在企业级应用开发中,数据分页是最基础却至关重要的功能。想象一下电商平台的商品列表,如果一次性加载所有商品数据,不仅会导致页面加载缓慢,更会消耗大量服务器资源。这就是分页查询存在的意义——它像图书馆的书架索引,让我们可以按需获取数据片段。
Spring Boot作为Java生态中最流行的应用框架,与MyBatis-Plus这一MyBatis增强工具的组合,为分页功能提供了优雅的解决方案。不同于传统的物理分页(直接使用LIMIT语句)或内存分页(先查全量再截取),MyBatis-Plus的分页插件实现了更智能的拦截器机制。
2. 基础环境搭建
2.1 依赖配置关键点
在pom.xml中,除了基础的spring-boot-starter-web外,需要特别注意这些依赖:
xml复制<!-- MyBatis-Plus核心库 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- 分页插件必须单独引入 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.5.3.1</version>
</dependency>
注意:很多开发者会遗漏mybatis-plus-extension,导致分页插件不生效。这两个artifact的版本号必须严格保持一致。
2.2 配置类中的关键代码
在Spring Boot启动类或配置类中添加分页拦截器:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
这里有几个技术细节需要注意:
- DbType需要根据实际数据库类型指定(如ORACLE、POSTGRE_SQL等)
- 可以配置多个InnerInterceptor,但分页插件通常放在首位
- 高版本中已废弃旧版PaginationInterceptor,必须使用新的拦截器体系
3. 分页查询实战详解
3.1 基础分页实现
Controller层典型写法:
java复制@GetMapping("/users")
public R<Page<User>> getUserPage(
@RequestParam(defaultValue = "1") int current,
@RequestParam(defaultValue = "10") int size) {
Page<User> page = new Page<>(current, size);
return R.ok(userService.page(page));
}
Service层实现:
java复制@Override
public Page<User> page(Page<User> page) {
return userMapper.selectPage(page, null);
}
这里有几个值得关注的实现细节:
- Page对象不仅包含分页参数,还会自动接收返回的分页数据
- 第二个参数是Wrapper条件构造器,可以为null表示无条件
- 返回的Page对象会包含total(总记录数)、records(当前页数据)等关键信息
3.2 带条件的分页查询
实际项目中,90%的分页都需要配合查询条件:
java复制@Override
public Page<User> pageWithCondition(Page<User> page, UserQuery query) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(query.getName()), User::getName, query.getName())
.ge(query.getMinAge() != null, User::getAge, query.getMinAge())
.le(query.getMaxAge() != null, User::getAge, query.getMaxAge());
return userMapper.selectPage(page, wrapper);
}
这种写法利用了MyBatis-Plus的条件构造器优势:
- Lambda表达式避免字段硬编码
- 条件方法第一个参数是boolean,实现动态SQL
- 支持链式调用,可读性强
4. 高级功能与性能优化
4.1 自定义分页SQL
当遇到复杂多表查询时,可能需要手写SQL:
java复制@Select("SELECT u.*, d.dept_name FROM user u LEFT JOIN department d ON u.dept_id = d.id ${ew.customSqlSegment}")
Page<UserDeptVO> selectUserDeptPage(Page<UserDeptVO> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
关键点说明:
- ${ew.customSqlSegment} 会自动拼接Wrapper生成的WHERE条件
- 返回类型Page的泛型可以是自定义VO
- 参数中必须使用@Param(Constants.WRAPPER)注解
4.2 分页性能优化策略
- COUNT优化:对于大表分页,可以重写count语句
java复制@Select("SELECT COUNT(1) FROM user u WHERE ${ew.customSqlSegment}")
Long selectCountCustom(@Param(Constants.WRAPPER) Wrapper<User> wrapper);
- 禁止不必要count:当只需要数据不需要总数时
java复制Page<User> page = new Page<>(current, size, false);
- 游标分页:对于超大数据量(需数据库支持)
java复制Cursor<User> cursor = userMapper.selectCursor(wrapper);
5. 常见问题排查指南
5.1 分页不生效的检查清单
- 确认已正确配置分页拦截器(见2.2节)
- 检查Page参数是否正确传入Mapper方法
- SQL日志中是否出现LIMIT语句
- 确保没有其他拦截器修改了SQL(如数据权限插件)
5.2 总数(total)不准确的解决方案
- 多表关联时COUNT结果可能异常,需要自定义COUNT SQL
- 子查询情况下可能需要使用COUNT(DISTINCT id)
- 某些数据库(如PostgreSQL)在复杂查询时需要特殊处理
5.3 性能问题诊断
当分页查询变慢时:
- 检查是否走了正确的索引(EXPLAIN分析)
- 深分页问题:
SELECT * FROM user LIMIT 1000000, 10- 解决方案1:使用id > last_id方式
- 解决方案2:使用子查询优化
6. 最佳实践与扩展思考
6.1 统一分页响应结构
建议封装统一的分页响应DTO:
java复制public class PageResult<T> {
private Long current;
private Long size;
private Long total;
private List<T> records;
public static <T> PageResult<T> success(Page<T> page) {
PageResult<T> result = new PageResult<>();
result.setCurrent(page.getCurrent());
result.setSize(page.getSize());
result.setTotal(page.getTotal());
result.setRecords(page.getRecords());
return result;
}
}
6.2 前端分页对接要点
- Ant Design Pro/Vue Element等前端框架对接示例
- 排序参数处理:
wrapper.orderBy(true, true, "create_time") - 特殊分页需求:如每页大小下拉选项配置
6.3 微服务场景下的分页考量
在分布式系统中:
- 跨服务分页的解决方案
- 数据聚合后的分页处理
- 缓存分页数据的策略
经过多个项目的实践验证,MyBatis-Plus的分页方案在大多数场景下都能提供良好的开发体验和性能表现。特别是在近期参与的某金融项目中,面对千万级用户表,通过合理配置和优化,分页查询响应时间始终保持在200ms以内。
