1. 问题现象:当MyBatis-Plus遇上PageHelper
最近在Spring Boot项目中同时使用MyBatis-Plus和PageHelper时,发现一个诡异现象:配置了MyBatis-Plus的分页插件后,执行分页查询时返回的却是全部数据。经过排查发现,当项目中同时存在PageHelper依赖时,MyBatis-Plus的分页功能就会失效,这种现象被开发者们戏称为"假分页"。
这种情况在实际开发中并不少见,特别是老项目迁移到MyBatis-Plus时,原有的PageHelper代码可能还保留着。两个分页插件在底层实现机制上存在冲突,导致分页功能异常。下面我们就来深入分析这个问题的根源。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 源码解析:冲突的底层机制
2.1 MyBatis-Plus分页原理
MyBatis-Plus的分页插件(PaginationInnerInterceptor)是通过拦截器机制实现的。当执行查询方法时,拦截器会检测是否需要分页:
java复制public class PaginationInnerInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms,
Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql) {
// 判断是否需要分页
if (parameter instanceof Map) {
Map<?, ?> parameterMap = (Map<?, ?>) parameter;
IPage<?> page = (IPage<?>) parameterMap.get("page");
if (page != null) {
// 执行分页逻辑
handlePage(ms, boundSql, page);
}
}
}
}
关键点在于,MyBatis-Plus的分页是通过在SQL执行前修改BoundSql对象,添加LIMIT语句实现的。
2.2 PageHelper工作原理
PageHelper同样是通过拦截器实现分页,但它采用的是ThreadLocal方式:
java复制public class PageHelper implements Interceptor {
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<>();
public static <E> Page<E> startPage(int pageNum, int pageSize) {
Page<E> page = new Page<>(pageNum, pageSize);
LOCAL_PAGE.set(page);
return page;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 从ThreadLocal获取分页参数
Page page = LOCAL_PAGE.get();
if (page != null) {
// 执行分页逻辑
return doIntercept(invocation, page);
}
return invocation.proceed();
}
}
PageHelper的分页参数是通过ThreadLocal传递的,这导致它在拦截器链中的执行顺序变得很关键。
2.3 冲突根源分析
当两个插件同时存在时,问题出在它们的执行顺序上:
- PageHelper的拦截器优先级较高,会先执行
- 它从ThreadLocal获取分页参数并修改SQL
- 然后MyBatis-Plus的拦截器执行,但此时ThreadLocal可能已被清除
- 最终导致分页参数丢失,返回全部数据
这种执行顺序的不确定性就是"假分页"问题的根源。
3. 解决方案:三种应对策略
3.1 方案一:统一使用MyBatis-Plus分页
推荐在新项目中完全使用MyBatis-Plus的分页方式:
java复制// 分页查询示例
Page<User> page = new Page<>(1, 10); // 当前页,每页大小
userMapper.selectPage(page, queryWrapper);
// 获取分页结果
List<User> records = page.getRecords();
long total = page.getTotal();
优势:
- 与MyBatis-Plus其他功能深度集成
- API设计更现代化
- 避免依赖冲突
3.2 方案二:调整拦截器顺序
如果必须使用PageHelper,可以调整拦截器执行顺序:
java复制@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 确保PaginationInnerInterceptor在最后执行
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
@Bean
public PageInterceptor pageInterceptor() {
PageInterceptor pageInterceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("helperDialect", "mysql");
pageInterceptor.setProperties(properties);
return pageInterceptor;
}
}
关键点:
- 确保MyBatis-Plus的拦截器最后执行
- 可能需要调整PageHelper的配置参数
3.3 方案三:完全移除PageHelper
对于已经全面使用MyBatis-Plus的项目,可以考虑完全移除PageHelper依赖:
xml复制<!-- 移除PageHelper依赖 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>${pagehelper.version}</version>
<exclusions>
<exclusion>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
</exclusion>
</exclusions>
</dependency>
然后逐步将项目中所有PageHelper的调用替换为MyBatis-Plus的分页API。
4. 深度对比:两种分页方式的差异
4.1 API设计对比
| 特性 | MyBatis-Plus分页 | PageHelper |
|---|---|---|
| 分页参数传递方式 | 方法参数 | ThreadLocal |
| 分页对象 | IPage接口实现类 | Page类 |
| 与ORM集成度 | 深度集成 | 相对独立 |
| 多数据源支持 | 完善 | 需要额外配置 |
4.2 性能对比
在实际测试中,两种分页方式的性能差异不大,但MyBatis-Plus在某些场景下表现更好:
- 大数据量分页时,MyBatis-Plus的count查询优化更好
- 复杂SQL场景下,MyBatis-Plus的解析更稳定
- 多数据源环境下,MyBatis-Plus的配置更简单
4.3 使用场景建议
- 新项目:推荐使用MyBatis-Plus分页
- 老项目迁移:逐步替换PageHelper
- 特殊需求:如需要PageHelper的某些高级特性,可考虑混合使用
5. 实战避坑指南
5.1 常见问题排查
-
分页失效:
- 检查是否同时存在两个分页插件
- 查看拦截器执行顺序
- 检查分页参数是否正确传递
-
count查询异常:
- 确认数据库方言配置正确
- 检查是否有复杂的join查询
- 考虑使用自定义count查询
-
性能问题:
- 大数据量时考虑优化count查询
- 避免不必要的字段查询
- 考虑缓存分页结果
5.2 最佳实践
-
统一分页方式:
整个项目最好统一使用一种分页方式,避免混用带来的维护成本。 -
分页参数校验:
java复制public Page<T> checkPageParam(int pageNum, int pageSize) { if (pageNum < 1) pageNum = 1; if (pageSize < 1 || pageSize > 100) pageSize = 10; return new Page<>(pageNum, pageSize); } -
自定义分页查询:
对于复杂查询,可以使用自定义SQL配合分页:java复制@Select("SELECT * FROM user WHERE ${ew.sqlSegment}") IPage<User> selectUserPage(IPage<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
5.3 高级技巧
-
优化count查询:
java复制// 在实体类上添加注解 @TableName(value = "user", resultMap = "userMap") public class User { // ... } // 在Mapper中定义count查询 @Select("SELECT COUNT(1) FROM user WHERE ${ew.sqlSegment}") Long selectCount(@Param(Constants.WRAPPER) Wrapper<User> wrapper); -
多表联查分页:
对于多表联查,建议:- 使用视图或子查询简化
- 考虑使用JOIN+GROUP BY优化
- 必要时放弃自动count查询,手动实现
-
前端分页配合:
前后端分页参数统一规范:java复制@GetMapping("/list") public Result<IPage<User>> list( @RequestParam(defaultValue = "1") int current, @RequestParam(defaultValue = "10") int size) { Page<User> page = new Page<>(current, size); return Result.success(userService.page(page)); }
6. 源码级调试技巧
当遇到分页问题时,可以通过调试源码来定位问题:
-
设置断点位置:
- PageInterceptor.intercept()
- PaginationInnerInterceptor.beforeQuery()
- MybatisPlusInterceptor.intercept()
-
关键观察点:
- SQL语句的变化过程
- 分页参数的传递路径
- 拦截器的执行顺序
-
日志配置:
在application.yml中添加:yaml复制logging: level: com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor: DEBUG com.github.pagehelper.PageInterceptor: DEBUG
通过源码调试,可以清晰看到两个插件是如何交互的,以及分页参数是在哪个环节丢失的。
7. 扩展思考:分页插件设计哲学
从这两个分页插件的设计中,我们可以学到一些架构设计的原则:
-
MyBatis-Plus的设计:
- 显式优于隐式(通过方法参数传递分页信息)
- 与框架深度集成
- 强调类型安全和API友好性
-
PageHelper的设计:
- 隐式上下文(ThreadLocal传递参数)
- 最小侵入性
- 强调灵活性和兼容性
这两种设计哲学各有利弊,也导致了它们在使用上的差异。理解这些差异有助于我们更好地选择和使用分页方案。
在实际项目实践中,我发现MyBatis-Plus的分页方式虽然学习曲线稍陡,但长期来看更易于维护。特别是当项目规模扩大、团队人员增多时,显式的API设计能减少很多沟通和维护成本。而PageHelper的ThreadLocal方式虽然使用简单,但在复杂的调用链路中容易出现问题,也不利于代码的可读性和可维护性。
