1. MybatisPlus分页机制深度解析
作为Java持久层框架的增强工具,MybatisPlus的分页功能一直是开发者高频使用的核心特性。但很多人在使用过程中都遇到过单页500条限制、分页失效等典型问题。今天我将结合6年实战经验,从原理到避坑全面剖析这个看似简单却暗藏玄机的功能。
先看一个基础分页示例:
java复制Page<User> page = new Page<>(1, 10); // 当前页1,每页10条
IPage<User> userPage = userMapper.selectPage(page, null);
这个简单的API调用背后,MybatisPlus其实完成了以下关键操作:
- 自动拦截SQL语句
- 改写为COUNT查询和LIMIT分页
- 处理不同数据库方言
- 封装统一的分页结果对象
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心实现原理剖析
2.1 分页拦截器工作机制
MybatisPlus通过PaginationInnerInterceptor实现分页逻辑。这个拦截器会在Executor执行SQL前进行拦截:
java复制public class PaginationInnerInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms...){
// 1. 判断是否需要分页
// 2. 自动执行COUNT查询
// 3. 改写原始SQL
}
}
关键点在于SQL改写策略:
- MySQL:
SELECT * FROM user→SELECT * FROM user LIMIT 0,10 - Oracle:使用ROWNUM嵌套查询
- PostgreSQL:
LIMIT 10 OFFSET 0
2.2 分页参数传递机制
分页对象通过ThreadLocal传递参数:
java复制public class Page<T> implements IPage<T> {
private long current = 1; // 当前页
private long size = 10; // 每页条数
private long total; // 总记录数
//...
}
注意:在SpringBoot中如果使用@RequestBody接收Page对象,需要特别处理参数绑定。
3. 高级使用技巧
3.1 自定义分页SQL
对于复杂查询,需要手动编写分页SQL:
xml复制<select id="selectUserPage" resultType="User">
SELECT * FROM user ${ew.customSqlSegment}
</select>
对应的Mapper接口:
java复制IPage<User> selectUserPage(Page<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
3.2 多表联查分页优化
遇到多表join时,推荐使用子查询分页:
sql复制SELECT t1.* FROM table1 t1
WHERE t1.id IN (
SELECT t2.id FROM table2 t2
LIMIT #{offset},#{size}
)
4. 性能优化方案
4.1 大数据量分页处理
当处理百万级数据时,传统LIMIT分页会出现性能问题。可以采用以下方案:
- 游标分页(适合顺序访问):
java复制// 使用lastId作为游标
QueryWrapper<User> wrapper = new QueryWrapper<>()
.gt("id", lastId)
.orderByAsc("id")
.last("LIMIT " + size);
- 延迟关联(减少回表):
sql复制SELECT t1.* FROM (
SELECT id FROM user WHERE status=1 LIMIT 100000,10
) t0 JOIN user t1 ON t0.id=t1.id
4.2 分页缓存策略
对于静态数据,可以缓存分页结果:
java复制@Cacheable(value = "userPage", key = "#current+'-'+#size")
public IPage<User> getPage(int current, int size) {
return userMapper.selectPage(new Page<>(current, size), null);
}
5. 典型问题排查指南
5.1 分页失效常见原因
- 拦截器未配置:
yaml复制mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDeleted
plugins:
- paginationInnerInterceptor
- Page参数位置错误:
java复制// 错误示例:page参数不在第一个位置
IPage<User> selectPage(@Param("ew") Wrapper<User> wrapper, Page<User> page);
// 正确写法
IPage<User> selectPage(Page<User> page, @Param("ew") Wrapper<User> wrapper);
5.2 单页500条限制突破
这是MybatisPlus的默认保护机制,可以通过自定义拦截器修改:
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor pagination = new PaginationInnerInterceptor();
pagination.setMaxLimit(1000L); // 修改为1000条
interceptor.addInnerInterceptor(pagination);
return interceptor;
}
6. 最佳实践建议
- 分页参数校验:
java复制public IPage<User> queryPage(PageQuery query) {
if(query.getPageSize() > 100) {
throw new BusinessException("单页条数不能超过100");
}
//...
}
- 前端分页配合:
javascript复制// Vue示例
const queryPage = async () => {
const res = await api.get('/users', {
params: {
current: this.pagination.current,
size: this.pagination.pageSize
}
})
this.data = res.data.records
this.pagination.total = res.data.total
}
- 特殊分页场景处理:
- 导出全部数据时建议单独接口
- 报表统计使用异步分页
- 移动端考虑实现上拉加载更多
在实际项目中,我推荐使用AOP统一处理分页参数和结果包装:
java复制@Around("execution(* *..*Service.*Page*(..))")
public Object handlePage(ProceedingJoinPoint joinPoint) throws Throwable {
Page<?> page = extractPageParam(joinPoint.getArgs());
Object result = joinPoint.proceed();
return new PageResult<>((IPage<?>) result);
}
分页功能看似简单,但在高并发、大数据量场景下需要特别注意性能优化和异常处理。建议在开发测试阶段就进行大数据量压测,提前发现潜在问题。
