1. MyBatis Plus分页插件入门指南
刚接触MyBatis Plus时,最让我头疼的就是分页查询的实现。传统的MyBatis需要手动编写limit语句,不同数据库语法还不一样,调试起来特别麻烦。直到发现了PaginationInnerInterceptor这个神器,我才真正体会到什么叫"开箱即用"。
PaginationInnerInterceptor是MyBatis Plus提供的分页拦截器,它能自动拦截SQL语句并添加分页逻辑。比如你执行一个普通的查询方法,它会在底层自动帮你加上LIMIT 10 OFFSET 20这样的语句。更棒的是,它支持多种数据库类型,MySQL、Oracle、PostgreSQL等都能自动适配,完全不用操心方言问题。
在实际项目中,我通常会这样初始化配置:
java复制@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页拦截器,指定数据库类型为MySQL
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
这个基础配置已经能满足80%的分页需求。但PaginationInnerInterceptor的强大之处在于,它还提供了许多实用的高级特性,比如防止恶意请求的超大分页、控制单页数据量上限等,这些我们后面会详细展开。
2. 分页插件核心配置详解
2.1 数据库类型适配
第一次使用PaginationInnerInterceptor时,我踩过一个坑:没有正确配置DbType。结果分页查询在测试环境(MySQL)跑得好好的,上了生产环境(Oracle)就直接报错。这是因为不同数据库的分页语法差异很大:
- MySQL使用LIMIT offset, size
- Oracle需要使用ROWNUM
- PostgreSQL使用LIMIT size OFFSET offset
解决方法很简单,在创建拦截器时指定正确的数据库类型:
java复制// 根据实际数据库配置
new PaginationInnerInterceptor(DbType.MYSQL);
// 或者
new PaginationInnerInterceptor(DbType.ORACLE);
如果项目需要支持多数据源,可以通过动态数据源切换时同步更新分页拦截器的数据库类型。我在一个多租户项目中就是这样处理的:
java复制// 在数据源切换后
PaginationInnerInterceptor innerInterceptor = (PaginationInnerInterceptor)interceptor.getInterceptors()
.stream()
.filter(i -> i instanceof PaginationInnerInterceptor)
.findFirst()
.orElse(null);
if(innerInterceptor != null){
innerInterceptor.setDbType(newDbType);
}
2.2 溢出页处理策略
去年双十一大促时,我们的商品查询接口突然出现性能问题。排查发现是有用户直接修改URL参数,请求了pageNumber=99999这样不存在的页码。虽然数据不存在,但数据库还是要扫描大量记录,导致CPU飙升。
这就是setOverflow(true)该出场的时候了:
java复制paginationInterceptor.setOverflow(true);
当设置为true时,如果请求的页码超过最大页数(比如总共10页但请求第11页),会自动返回第一页数据。这就像电梯的楼层按钮,按了不存在的楼层会自动回到1楼一样。
我建议在Web项目中都开启这个配置,可以有效防止恶意爬虫或者参数篡改导致的性能问题。它的实现原理也很巧妙:
java复制// 伪代码展示overflow逻辑
if (currentPage > totalPage && overflow) {
currentPage = 1; // 回到第一页
rebuildLimit(); // 重新构建分页SQL
}
3. 高级特性实战技巧
3.1 单页数据量限制
记得有一次安全扫描,报告说我们的接口存在DoS风险,因为攻击者可以传pageSize=10000这样的大数值。这正是setMaxLimit的用武之地:
java复制// 限制单页最多100条记录
paginationInterceptor.setMaxLimit(100L);
设置后,即使客户端传size=1000,实际查询也只会返回100条。这就像自助餐厅的"每人每次限取三份"规定,防止有人一次性搬空整个餐台。
实际开发中,我通常会根据业务场景设置不同的上限:
- 管理后台可以设置大一些(如500)
- 移动端列表建议50-100
- 图表数据可以更小(如20)
java复制// 根据不同业务场景动态设置
if (request.getRequestURI().contains("/mobile/")) {
paginationInterceptor.setMaxLimit(50L);
} else if (request.getRequestURI().contains("/admin/")) {
paginationInterceptor.setMaxLimit(500L);
}
3.2 自定义分页参数名
前后端联调时,经常遇到前端传的是pageIndex/pageSize,而后端默认接收的是current/size。与其让前端改,不如后端适配:
java复制// 在配置类中添加这个Bean
@Bean
public PaginationInnerInterceptor paginationInterceptor() {
PaginationInnerInterceptor interceptor = new PaginationInnerInterceptor();
// 设置请求参数名
interceptor.setDbType(DbType.MYSQL);
interceptor.setOptimizeJoin(true);
// 关键配置:自定义参数名
interceptor.setMaxLimit(100L);
interceptor.setOverflow(true);
return interceptor;
}
更灵活的做法是实现自定义的Page对象:
java复制public class CustomPage<T> extends Page<T> {
@JsonProperty("page_index")
private long current;
@JsonProperty("page_size")
private long size;
// 构造方法...
}
4. 分页查询实战演示
4.1 基础分页查询
在DAO层,使用MyBatis Plus的分页查询简单得不可思议:
java复制// 创建分页参数(第2页,每页10条)
Page<User> page = new Page<>(2, 10);
// 执行查询
userMapper.selectPage(page, null);
执行后,page对象会包含所有分页信息:
java复制page.getRecords(); // 当前页数据列表
page.getCurrent(); // 当前页码
page.getSize(); // 每页大小
page.getTotal(); // 总记录数
page.getPages(); // 总页数
page.hasNext(); // 是否有下一页
page.hasPrevious(); // 是否有上一页
我在项目中最喜欢的是它的链式调用方式:
java复制userMapper.selectPage(new Page<>(1, 10),
Wrappers.<User>lambdaQuery()
.gt(User::getAge, 18)
.orderByAsc(User::getName))
.getRecords()
.forEach(System.out::println);
4.2 自定义分页SQL
遇到复杂查询时,可能需要手写SQL。MyBatis Plus同样支持:
xml复制<!-- mapper.xml -->
<select id="selectUserPage" resultType="User">
SELECT * FROM user WHERE age > #{age}
ORDER BY create_time DESC
</select>
对应的Mapper接口:
java复制// 方法参数必须用@Param("page")标注
IPage<User> selectUserPage(@Param("page") Page<User> page, @Param("age") int age);
调用方式:
java复制Page<User> page = new Page<>(1, 10);
userMapper.selectUserPage(page, 18);
// page对象同样包含所有分页信息
4.3 多表联查分页
联表查询的分页要特别注意:如果直接使用JOIN,分页结果可能会不准确。我推荐两种解决方案:
- 使用子查询先分页再关联:
sql复制SELECT a.*, b.extra_info
FROM (
SELECT * FROM main_table
WHERE conditions
LIMIT 0, 10
) a LEFT JOIN extra_table b ON a.id = b.main_id
- 使用MyBatis Plus的selectPage优化:
java复制// 先查询主表分页
Page<Main> mainPage = mainMapper.selectPage(page, queryWrapper);
// 再批量查询关联数据
List<Extra> extras = extraMapper.selectBatchIds(
mainPage.getRecords().stream()
.map(Main::getId)
.collect(Collectors.toList())
);
5. 性能优化与常见问题
5.1 避免COUNT查询
在数据量大的表中,COUNT操作可能非常耗时。如果不需要知道总页数,可以关闭这个功能:
java复制Page<User> page = new Page<>(1, 10);
// 关闭优化COUNT SQL
page.setSearchCount(false);
userMapper.selectPage(page, null);
这样生成的SQL就不会包含COUNT语句,性能能提升50%以上。我在一个500万数据的订单表中测试过:
- 开启searchCount:1200ms
- 关闭searchCount:400ms
5.2 分页缓存策略
对于变化不频繁的数据,可以考虑缓存分页结果。我的常用方案:
java复制public Page<User> getUsersWithCache(int pageNum, int pageSize) {
String cacheKey = "users:page:" + pageNum + ":" + pageSize;
Page<User> page = (Page<User>) redisTemplate.opsForValue().get(cacheKey);
if (page == null) {
page = userMapper.selectPage(new Page<>(pageNum, pageSize), null);
redisTemplate.opsForValue().set(cacheKey, page, 5, TimeUnit.MINUTES);
}
return page;
}
注意要设置合理的过期时间,并在数据变更时清除相关缓存。
5.3 常见问题排查
- 分页失效怎么办?
- 检查是否正确配置了拦截器
- 确认Page对象作为第一个参数
- 查看生成的SQL日志
- 排序不生效?
- 检查是否有多个ORDER BY冲突
- 确认Wrapper中的orderBy方法调用正确
- 性能突然下降?
- 检查是否查询了所有字段
- 确认是否有合适的索引
- 考虑使用searchCount(false)
我在实际项目中总结了一个检查清单:
- [ ] 拦截器配置正确
- [ ] 数据库类型匹配
- [ ] Page对象创建正确
- [ ] 没有N+1查询问题
- [ ] 关键字段有索引