1. MyBatis Plus分页拦截器深度解析
最近在重构一个老项目的分页功能时,我再次被MyBatis Plus的分页机制"教育"了一番。这个看似简单的分页拦截器,在实际业务场景中却藏着不少门道。今天我就结合自己踩过的坑,详细拆解如何正确使用MyBatis Plus的分页拦截器,特别是解决Page对象分页和total数量不准这两个高频痛点。
先说说典型场景:当我们需要实现列表分页查询时,通常会创建一个Page对象,设置当前页码和每页条数,然后交给MyBatis Plus的mapper执行查询。理想情况下,拦截器应该自动帮我们完成两件事:1) 对SQL进行分页改写;2) 执行count查询获取总记录数。但现实往往骨感 - 你可能遇到过total为0、分页失效或者性能问题。接下来我们就深入解决这些问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分页拦截器核心原理
2.1 拦截器工作机制
MyBatis Plus的分页拦截器(PaginationInnerInterceptor)本质上是一个MyBatis的插件,它通过拦截Executor的query方法实现功能。当检测到方法参数中包含IPage对象时,拦截器会:
- 对原始SQL进行方言适配的分页改写(如MySQL会添加LIMIT)
- 生成COUNT查询语句获取总记录数
- 将分页结果和total设置回IPage对象
关键点在于:拦截器只对mapper方法的第一个IPage参数生效,且要求该参数必须位于第一个位置。这是很多同学分页失效的常见原因。
2.2 Page对象的内存分页陷阱
MyBatis Plus提供了两种分页模式:
- 物理分页:通过拦截器改写SQL(推荐)
- 内存分页:先查全部数据再手动截取(性能灾难)
特别注意:如果你在Service层创建了Page对象但没传给mapper,而是在查询结果后手动setRecords,这时就会退化成内存分页!我曾在一个百万级数据表上犯过这个错误,直接导致OOM。
3. 正确配置分页拦截器
3.1 基础配置方案
Spring Boot项目中,配置分页拦截器只需一个配置类:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页拦截器
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
paginationInterceptor.setDbType(DbType.MYSQL); // 根据实际数据库类型设置
paginationInterceptor.setOverflow(true); // 超出总页数后回到第一页
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
3.2 关键参数解析
setMaxLimit():设置单页最大条数,防止恶意传参(如pageSize=10000)setOptimizeJoin():优化联表查询的count语句,默认truesetDialect():指定数据库方言,5.3.1+版本推荐用setDbType
警告:在多数据源环境下,必须确保分页拦截器的dialect与执行SQL的数据源匹配,否则会导致分页SQL语法错误。
4. 解决total数量不准问题
4.1 常见原因分析
- 自定义SQL未使用Page参数:
xml复制<!-- 错误示例 -->
<select id="selectPage" resultType="User">
SELECT * FROM user WHERE age > #{age}
</select>
<!-- 正确写法 -->
<select id="selectPage" resultType="User">
SELECT * FROM user WHERE age > #{param2.age}
</select>
- 联表查询未优化count语句:
对于复杂联表查询,默认的SELECT COUNT(1)可能性能极差。可以通过@SqlParser注解或xml配置自定义count语句:
xml复制<select id="selectUserPage" resultType="User">
SELECT u.*, d.dept_name
FROM user u LEFT JOIN dept d ON u.dept_id = d.id
</select>
<select id="selectUserPage_count" resultType="Long">
SELECT COUNT(1) FROM user u <!-- 只统计主表 -->
</select>
4.2 高性能count方案
对于百万级以上的表,建议:
- 使用缓存count值(适合变化不频繁的数据)
- 添加
where 1=1条件让优化器忽略缓存(解决count走错索引问题) - 对大表使用
EXPLAIN SELECT COUNT先分析执行计划
5. 特殊场景处理技巧
5.1 多表关联分页
当需要分页查询关联表数据时,推荐使用子查询先分页再关联:
sql复制SELECT t1.*, t2.name
FROM (
SELECT * FROM main_table
WHERE conditions
LIMIT #{offset}, #{pageSize}
) t1
LEFT JOIN other_table t2 ON t1.id = t2.main_id
这种写法在MySQL等数据库上性能显著优于直接联表分页。
5.2 分页缓存策略
对于高频访问的分页接口,可以采用二级缓存:
java复制@Cacheable(value = "userPage", key = "#page.current+'-'+#page.size")
public Page<User> selectUserPage(Page<User> page, QueryWrapper<User> wrapper) {
return userMapper.selectPage(page, wrapper);
}
注意:缓存Page对象时需要实现Serializable接口,且要考虑数据一致性问题。
6. 实战问题排查指南
6.1 分页失效检查清单
- 确认方法参数中Page对象是第一个参数
- 检查是否配置了分页拦截器
- 查看最终执行的SQL日志(开启mybatis-plus.sql-log=true)
- 确保没有在拦截器之后添加其他拦截器覆盖了分页逻辑
6.2 性能问题优化
当分页查询变慢时:
- 检查count语句是否使用了索引
- 深分页优化:使用
where id > last_id limit size替代limit offset,size - 对于ES等数据源,考虑使用search_after代替传统分页
7. 新版特性与迁移建议
MyBatis Plus 3.5.x+版本中:
- 移除了@SqlParser注解,改为通过@InterceptorIgnore控制
- 新增了JsqlParserCountOptimize优化器
- 对PostgreSQL等数据库的分页支持更完善
升级时需要注意:
- 旧的PaginationInterceptor已弃用,需改用PaginationInnerInterceptor
- 如果使用自定义count语句,需要检查新版的语法兼容性
最后分享一个调试技巧:在application.yml中添加以下配置,可以清晰看到分页SQL的生成过程:
yaml复制mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
通过日志你能直观看到拦截器如何改写你的SQL,这对排查分页问题非常有帮助。记住,分页看似简单,但在高并发、大数据量场景下,每一个细节都可能成为性能瓶颈。
