1. 问题背景:为什么分页查询会失效?
SpringBoot + MyBatis-Plus组合是Java后端开发的主流技术栈,分页查询作为高频操作,却经常出现各种"诡异"失效情况。最近在重构公司商品管理系统时,我就遇到了分页配置看似正确但实际返回全部数据的坑。经过排查发现,MyBatis-Plus的分页机制比表面看到的要复杂得多。
分页失效的典型表现有:
- 配置了分页插件但查询结果仍是全表数据
- 前端传了分页参数但后端未生效
- 多表联查时分页结果异常
- 自定义SQL与分页插件冲突
这些问题的本质原因可以归结为:MyBatis-Plus的分页实现涉及拦截器、参数处理、SQL改写等多个环节,任何一个环节配置不当都会导致分页失效。下面我们就从实际案例出发,彻底解决这些痛点问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心配置检查:分页插件必须正确初始化
2.1 基础配置的常见误区
很多开发者以为只要引入MyBatis-Plus依赖就自动启用分页功能,这是第一个认知误区。实际上必须显式配置分页插件:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 必须添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
常见配置错误包括:
- 忘记添加
PaginationInnerInterceptor - 未指定正确的数据库类型(如DbType.MYSQL)
- 将分页插件添加到其他拦截器之后(顺序影响执行)
关键点:分页插件必须作为第一个InnerInterceptor添加,否则可能被其他拦截器影响
2.2 多数据源的特殊处理
在多数据源环境下,需要为每个数据源单独配置分页插件:
java复制@Bean
public MybatisPlusInterceptor ds1Interceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
@Bean
public MybatisPlusInterceptor ds2Interceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.ORACLE));
return interceptor;
}
不同数据库的分页语法差异很大(MySQL的LIMIT vs Oracle的ROWNUM),必须正确指定DbType。
3. 分页参数传递的正确姿势
3.1 控制器层的最佳实践
前端传递分页参数时,推荐使用统一封装:
java复制@GetMapping("/list")
public R<Page<User>> listUsers(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
return R.success(userService.page(page));
}
常见问题包括:
- 参数未使用
@RequestParam导致接收不到 - 未设置默认值导致空指针异常
- 直接使用Map接收参数失去类型安全
3.2 Service层的优化方案
在Service层可以添加查询条件:
java复制public Page<User> pageUsers(Page<User> page, UserQuery query) {
return lambdaQuery()
.eq(StringUtils.isNotBlank(query.getName()), User::getName, query.getName())
.page(page);
}
这里有个重要细节:Page对象必须作为第一个参数传入,否则分页会失效。
4. 自定义SQL的分页处理
4.1 XML中编写分页SQL
当使用自定义SQL时,MyBatis-Plus会自动改写SQL添加分页语句:
xml复制<select id="selectUserPage" resultType="User">
SELECT * FROM user WHERE status = 1
</select>
但要注意:
- 不能手动添加LIMIT语句
- 参数必须包含Page对象
- 返回类型可以是Page或List
4.2 注解方式的分页查询
使用@Select注解时也需要遵循相同规则:
java复制@Select("SELECT * FROM user WHERE dept_id = #{deptId}")
Page<User> selectByDept(Page<User> page, @Param("deptId") Long deptId);
实测踩坑:如果忘记在参数中添加Page对象,即使SQL写得再完美,分页也会失效。
5. 多表联查的特殊情况处理
5.1 一对多查询的分页陷阱
典型场景:查询用户及其订单列表。如果直接在SQL中使用JOIN:
sql复制SELECT u.*, o.* FROM user u LEFT JOIN order o ON u.id = o.user_id
这样分页会出现数据错乱(一个用户有多条订单记录)。正确做法是:
- 先分页查询用户
- 再批量查询关联订单
- 手动组装结果
java复制Page<User> userPage = userMapper.selectPage(page, queryWrapper);
List<Long> userIds = userPage.getRecords().stream().map(User::getId).toList();
Map<Long, List<Order>> orderMap = orderService.listByUserIds(userIds);
5.2 使用子查询优化
对于复杂场景,可以在WHERE中使用子查询:
sql复制SELECT * FROM order
WHERE user_id IN (
SELECT id FROM user WHERE status = 1 LIMIT 10 OFFSET 0
)
这样能保证主查询的分页准确性。
6. 高级场景与性能优化
6.1 大数据量分页优化
当处理百万级数据时,传统LIMIT分页会出现性能问题。推荐方案:
- 使用索引覆盖扫描:
sql复制SELECT * FROM user WHERE id > 10000 LIMIT 10
-
前端实现"加载更多"模式,避免跳页
-
使用游标分页(基于最后一条记录的ID)
6.2 分布式环境下的分页
在分库分表场景中,常规分页会失效。解决方案:
- 使用ShardingSphere等中间件
- 采用ES等搜索引擎
- 预计算+缓存分页结果
7. 完整解决方案代码示例
7.1 基础配置类
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件必须放在第一位
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 其他插件...
return interceptor;
}
}
7.2 控制器示例
java复制@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/page")
public R<Page<User>> pageUsers(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
UserQuery query) {
Page<User> page = new Page<>(pageNum, pageSize);
return R.success(userService.pageUsers(page, query));
}
}
7.3 Service实现
java复制@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public Page<User> pageUsers(Page<User> page, UserQuery query) {
return lambdaQuery()
.eq(StringUtils.isNotBlank(query.getName()), User::getName, query.getName())
.eq(query.getStatus() != null, User::getStatus, query.getStatus())
.page(page);
}
}
8. 调试技巧与常见问题排查
当分页失效时,按以下步骤排查:
- 检查SQL日志,确认是否输出了分页语句
- 调试查看Page参数是否传递到了Mapper层
- 确认是否有多余的LIMIT语句导致冲突
- 检查MyBatis-Plus版本是否兼容
可以在application.yml中添加配置查看完整SQL:
yaml复制mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
我在实际项目中发现,当使用MyBatis原生API(如SqlSession)时,分页插件会失效,这种情况下需要手动处理分页逻辑。
分页查询看似简单,但实际开发中会遇到各种边界情况。建议在项目初期就建立统一的分页处理规范,避免后期大量重构。对于复杂查询,可以考虑使用视图或存储过程来简化分页逻辑。
