【MPJ实战】通用一对多分页工具类彻底解决多表连接分页失真问题适用场景MyBatis-Plus-JoinMPJ一对多关系分页查询、解决分页数据失真/N1查询问题核心价值通用工具类、无手写SQL、链式调用、支持多子表、兼容MP/MPJ所有版本一、前言 在企业级开发中一对多关系的分页查询是高频场景用户→订单、订单→商品、部门→员工。但直接使用多表JOIN查询会出现致命问题分页数据失真主表1条数据对应子表多条数据分页结果条数错误、数据重复N1查询性能灾难先查主表再循环查子表接口性能极差MPJ原生缺陷MPJ仅支持一对一映射无法自动装配一对多集合MyBatis-Plus-JoinMPJ解决了多表连接的便捷性但原生不支持一对多分页自动映射。本文基于MPJ封装通用一对多分页工具类彻底解决上述痛点开箱即用二、痛点深度分析 ❌问题场景具体表现连表分页失真主表子表JOIN后结果集膨胀分页条数与预期不符N1查询循环遍历主表数据查询子表数据库压力剧增MPJ不支持一对多分页查询仅返回平铺数据无法封装成主表VO子表集合代码冗余每个一对多分页都手写分组、映射逻辑三、解决方案思路 核心主表精准分页 子表批量查询 自动映射集合用MPJ查询主表分页数据保证分页条数精准提取主表ID一次性批量查询所有子表数据无N1工具类自动分组、映射子表数据到主表VO最终封装通用工具类支持多子表、链式调用、零侵入。四、核心通用工具类直接复制OneToManyPageUtil.java支持多子表映射、自定义参数、空数据自动处理、MP/MPJ全版本兼容importcom.baomidou.mybatisplus.core.metadata.IPage;importjava.util.Collections;importjava.util.List;importjava.util.Map;importjava.util.function.BiConsumer;importjava.util.function.Function;importjava.util.stream.Collectors;/** * 一对多分页通用工具类支持多子表 自定义参数顺序 * 解决主表分页 多子表批量查询 自动映射集合彻底解决N1/分页失真 * 适用MP/MPJ 所有版本 * author 你的ID */publicclassOneToManyPageUtil{/** * 初始化构建器仅传入分页对象主表ID提取函数在withSub中定义 */publicstaticVOBuilderVOpage(IPageVOvoPage){returnnewBuilder(voPage);}/** * 内部构建器类支持链式添加多个子表映射 * 第一次添加子表时需指定主表ID提取函数后续子表自动复用 */publicstaticclassBuilderVO{privatefinalIPageVOvoPage;privatefinalListVOrecords;privateFunctionVO,?mainIdGetter;// 缓存主表ID提取函数多子表复用privateBuilder(IPageVOvoPage){this.voPagevoPage;this.recordsvoPage.getRecords();}/** * 添加子表映射首次调用需指定主表ID提取函数 * param batchSubQuery 批量查询子表函数如 orderService::getOrderList * param mainIdGetter 主表ID提取函数如 UserVO::getId * param subIdGetter 子表关联主表ID函数如 OrderInfo::getUserId * param subListSetter VO设置子集合函数如 UserVO::setOrders * return 构建器自身支持链式调用 */publicSub,IDBuilderVOwithSub(Function?superListID,?extendsListSubbatchSubQuery,FunctionVO,IDmainIdGetter,Function?superSub,?extendsIDsubIdGetter,BiConsumer?superVO,?superListSubsubListSetter){if(records.isEmpty())returnthis;this.mainIdGettermainIdGetter;// 缓存主表ID函数供后续子表复用doMapping(batchSubQuery,mainIdGetter,subIdGetter,subListSetter);returnthis;}/** * 添加子表映射非首次调用自动复用之前的主表ID提取函数 * param batchSubQuery 批量查询子表函数 * param subIdGetter 子表关联主表ID函数 * param subListSetter VO设置子集合函数 * return 构建器自身支持链式调用 */publicSubBuilderVOwithSub(Function?superList?,?extendsListSubbatchSubQuery,Function?superSub,?subIdGetter,BiConsumer?superVO,?superListSubsubListSetter){if(records.isEmpty()||mainIdGetternull)returnthis;// 复用首次传入的主表ID提取函数FunctionVO,ObjectcastedMainIdGettervo-(Object)mainIdGetter.apply(vo);doMapping(batchSubQuery,castedMainIdGetter,subIdGetter,subListSetter);returnthis;}/** * 执行一对多映射的核心逻辑 */privateSub,IDvoiddoMapping(Function?superListID,?extendsListSubbatchSubQuery,FunctionVO,IDmainIdGetter,Function?superSub,?extendsIDsubIdGetter,BiConsumer?superVO,?superListSubsubListSetter){// 1. 提取主表IDListIDmainIdsrecords.stream().map(mainIdGetter).collect(Collectors.toList());// 2. 批量查询子表1次查询无N1ListSubsubListbatchSubQuery.apply(mainIds);// 3. 子表按主ID分组MapID,ListSubsubMapsubList.stream().collect(Collectors.groupingBy(subIdGetter));// 4. 自动映射子集合records.forEach(vo-subListSetter.accept(vo,subMap.getOrDefault(mainIdGetter.apply(vo),Collections.emptyList())));}/** * 完成构建返回处理后的分页VO */publicIPageVObuild(){returnvoPage;}}}五、实战使用教程 5.1 依赖准备Maven确保项目已引入MPJ依赖!-- MyBatis-Plus-Join 核心依赖 --dependencygroupIdcom.github.yulichang/groupIdartifactIdmybatis-plus-join-boot-starter/artifactIdversion1.5.5/version/dependency5.2 子表批量查询方法子表Service必须提供根据主表ID集合批量查询的方法importjava.util.List;/** * 订单Service接口 */publicinterfaceOrderService{/** * 根据用户ID集合批量查询订单列表 * param userIds 主表用户ID集合 * return 订单列表 */ListOrderInfogetOrderList(ListLonguserIds);}5.3 业务层一对多分页查询直接复用原有MPJ查询逻辑仅需用工具类增强映射ServicepublicclassUserServiceImplimplementsUserService{ResourceprivateUserMapperuserMapper;ResourceprivateOrderServiceorderService;/** * 用户订单 一对多分页查询 */OverridepublicIPageUserVOpageUser(IntegerpageNum,IntegerpageSize){// 1. 构建分页对象PageUserVOpagenewPage(pageNum,pageSize);// 2. MPJ原生查询主表无手写SQL原有逻辑不变MPJLambdaWrapperUserwrappernewMPJLambdaWrapperUser().selectAll(User.class)// 查询主表所有字段.orderByAsc(User::getId);// 排序// 3. MPJ分页查询主表数据IPageUserVOpageVouserMapper.selectJoinPage(page,UserVO.class,wrapper);// 4. 工具类链式映射子表核心returnOneToManyPageUtil.page(pageVo)// 映射订单子表.withSub(orderService::getOrderList,// 批量查询子表方法UserVO::getId,// 主表ID提取OrderInfo::getUserId,// 子表关联主表IDUserVO::setOrders// VO设置子集合)// 支持链式添加多个子表如用户→地址、用户→优惠券// .withSub(addressService::getAddressList, AddressInfo::getUserId, UserVO::setAddressList).build();}}六、工具类核心原理解析 工具类仅4步完成一对多映射性能拉满提取主表ID从分页结果中提取所有主表ID批量查子表调用子表方法一次性查询所有关联数据1次查询子表分组按主表ID将子表数据分组为Map主ID, 子表列表自动装配遍历主表数据将分组后的子表数据赋值给VO集合属性七、核心优势 ✅分页绝对精准主表独立分页彻底解决数据失真性能极致无N1查询仅1次主表N次子表批量查询通用无侵入不修改原有MPJ代码适配所有一对多场景多子表支持链式调用withSub轻松映射多个子表零手写SQL完全基于MPJ lambda维护成本极低空值安全自动处理空数据无需手动判空八、注意事项 ⚠️子表查询方法必须接收List主表ID返回子表列表主表VO需要定义子表集合属性如ListOrderInfo orders首次调用withSub需指定主表ID提取函数后续子表自动复用兼容MyBatis-Plus/MyBatis-Plus-Join所有版本九、总结 本文通过通用一对多分页工具类MPJ完美解决了多表一对多分页查询的所有痛点✅ 分页失真✅ N1查询✅ 一对多映射失效✅ 代码冗余重复该工具类是企业级开发的最佳实践代码简洁、通用性强、性能优异直接复制即可使用互动环节如果对你有帮助欢迎点赞、收藏、关注后续会分享更多MPJ实战技巧~有问题可以在评论区留言我会第一时间回复