1. 项目概述
在业务系统开发中,分页查询是最基础也最常用的功能之一。当遇到需要关联多张表进行分页查询的场景时,传统的MyBatis实现方式往往需要编写大量重复代码。MyBatisPlus作为MyBatis的增强工具,提供了开箱即用的分页插件,但在处理复杂多表联查时,仍需要开发者进行适当扩展。
本文将详细介绍如何基于MyBatisPlus实现自定义分页查询,解决多表联查场景下的分页难题。通过实际案例演示,你将掌握如何突破单页500条限制等常见问题,构建高效稳定的分页查询方案。
2. 核心需求解析
2.1 多表联查分页的痛点
在多表关联查询场景下,直接使用MyBatisPlus的自动分页功能会遇到几个典型问题:
- 性能瓶颈:当关联表数据量大时,自动生成的count查询可能非常耗时
- 结果映射:复杂联查结果需要自定义ResultMap处理
- 排序问题:多表字段排序需要特殊处理
- 单页限制:默认配置下单页最多返回500条记录
2.2 解决方案设计思路
我们的自定义分页方案将围绕以下几个核心点展开:
- 自定义分页SQL,优化count查询性能
- 使用MyBatisPlus的Interceptor机制扩展分页功能
- 实现灵活的多表字段排序支持
- 通过配置调整突破单页限制
3. 技术实现详解
3.1 基础环境准备
首先确保项目中已正确引入MyBatisPlus依赖(以Maven为例):
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>配置分页插件(Spring Boot配置类中):
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }3.2 自定义分页查询实现
3.2.1 创建分页参数类
扩展MyBatisPlus的Page类,添加多表查询特有参数:
public class CustomPage<T> extends Page<T> { // 联表查询条件 private Map<String, Object> joinConditions; // 排序字段映射(表别名.字段名) private Map<String, String> sortMapping; // 构造方法 public CustomPage(long current, long size) { super(current, size); } // getter/setter省略 }3.2.2 编写自定义分页Mapper方法
在Mapper接口中定义自定义分页查询方法:
public interface UserMapper extends BaseMapper<User> { @Select("<script>" + "SELECT u.*, d.dept_name " + "FROM user u LEFT JOIN department d ON u.dept_id = d.id " + "<where> ${ew.customSqlSegment} </where>" + "</script>") IPage<User> selectUserWithDeptPage(IPage<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper); }3.2.3 实现Service层分页逻辑
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override public IPage<UserDto> selectUserWithDeptPage(CustomPage<User> page) { // 构建查询条件 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 处理联表查询条件 if (page.getJoinConditions() != null) { page.getJoinConditions().forEach((k, v) -> { wrapper.eq(k, v); }); } // 处理排序 if (StringUtils.isNotBlank(page.getOrderByField())) { String orderField = page.getSortMapping().get(page.getOrderByField()); wrapper.orderBy(true, page.isAsc(), orderField); } // 执行分页查询 IPage<User> userPage = baseMapper.selectUserWithDeptPage(page, wrapper); // 转换为DTO并返回 return userPage.convert(user -> { UserDto dto = new UserDto(); BeanUtils.copyProperties(user, dto); // 处理额外字段 return dto; }); } }3.3 突破单页500条限制
MyBatisPlus默认限制单页最大500条记录,可通过以下方式调整:
- 全局配置(application.yml):
mybatis-plus: global-config: db-config: max-limit: 1000 # 设置最大单页记录数- 动态调整(在查询前设置):
// 在Service方法中 page.setMaxLimit(1000L); // 动态设置本次查询的最大记录数4. 高级优化技巧
4.1 分页性能优化
对于大数据量的分页查询,推荐使用以下优化方案:
- 延迟关联技术:
SELECT * FROM user u JOIN ( SELECT id FROM user WHERE [条件] ORDER BY [排序字段] LIMIT [offset], [size] ) AS tmp ON u.id = tmp.id JOIN department d ON u.dept_id = d.id- 游标分页(适合深度分页):
// 使用SQL_CALC_FOUND_ROWS获取总数 @Select("SELECT SQL_CALC_FOUND_ROWS u.* FROM user u LIMIT #{offset}, #{size}") List<User> selectByCursor(@Param("offset") long offset, @Param("size") long size); @Select("SELECT FOUND_ROWS()") long selectFoundRows();4.2 动态表名支持
在多租户等需要动态表名的场景下,可以这样实现:
@Select("SELECT * FROM ${tableName} ${ew.customSqlSegment}") IPage<User> selectByDynamicTable(IPage<User> page, @Param("tableName") String tableName, @Param(Constants.WRAPPER) Wrapper<User> wrapper);5. 常见问题与解决方案
5.1 分页结果总数不准确
问题现象:count查询结果与实际记录数不符
解决方案:
- 检查SQL中是否有GROUP BY子句
- 确认关联条件是否正确
- 使用自定义count查询:
@Select({"<script>", "SELECT COUNT(1) FROM user u", "LEFT JOIN department d ON u.dept_id = d.id", "<where> ${ew.customSqlSegment} </where>", "</script>"}) Long selectUserWithDeptCount(@Param(Constants.WRAPPER) Wrapper<User> wrapper);5.2 排序字段无效
问题现象:指定的排序字段未生效
排查步骤:
- 检查SQL中是否有该字段
- 确认字段名是否正确(包括表别名)
- 检查是否有SQL注入过滤导致字段名被修改
5.3 性能问题排查
对于慢查询,建议:
- 添加合适的索引
- 使用EXPLAIN分析执行计划
- 考虑使用缓存计数结果
6. 最佳实践建议
- 分页大小控制:根据业务场景合理设置pageSize,一般建议不超过1000
- 索引优化:确保分页查询条件和排序字段都有合适索引
- 缓存策略:对于变化不频繁的数据,可缓存分页结果
- 监控报警:对慢分页查询建立监控机制
重要提示:在调整max-limit参数时,需评估系统性能和内存消耗,避免单次查询返回过多数据导致OOM。
7. 扩展思考
7.1 前端分页配合
实现前后端分离的分页交互时,建议:
- 统一分页参数命名(如pageNum/pageSize)
- 返回标准分页结构:
{ "code": 200, "data": { "records": [...], "total": 1000, "size": 10, "current": 1 } }7.2 分布式环境下的分页
在分布式系统中,分页查询需要特别注意:
- 避免跨节点排序分页
- 考虑使用Elasticsearch等专门的分页查询方案
- 对于精确分页需求,可能需要全量数据归并
在实际项目中,我们通过这套自定义分页方案成功处理了用户中心模块中涉及5张表关联的复杂分页查询,性能较原生实现提升了3倍以上。关键点在于合理设计count查询和利用MyBatisPlus的扩展机制。