MyBatis一对多分页问题解决方案与优化实践
1. PageHelper一对多分页问题本质剖析在MyBatis框架中使用PageHelper进行一对多关联查询时分页结果不准确是个经典难题。问题的根源在于SQL执行机制当主表一和子表多进行JOIN操作时数据库返回的结果集会因为关联关系而膨胀。例如主表有10条记录每条关联5条子表记录实际返回50行数据。PageHelper基于物理分页机制LIMIT截取前10条时可能只得到2-3个完整的主表记录。我处理过的一个典型案例是订单系统订单表主表与订单项表子表1:N关系。当使用常规JOIN查询配合PageHelper分页时页面显示订单数量总是不稳定有时甚至出现同一订单被分割在不同页面的情况。这种体验对用户而言是完全不可接受的。2. 主流解决方案对比与选型2.1 方案一内存分页不推荐// 伪代码示例危险操作 ListOrder orders orderMapper.selectAllWithItems(); // 获取全部数据 PageInfoOrder pageInfo new PageInfo(orders); pageInfo.setList(orders.stream() .skip((pageNum-1)*pageSize) .limit(pageSize) .collect(Collectors.toList()));这种方案虽然简单但在数据量达到万级时就会引发OOM。我曾见过某电商系统在促销期间因此崩溃教训惨痛。2.2 方案二子查询分页推荐方案核心思路分两步先对主表进行分页查询再对每个主表记录执行子查询获取关联数据!-- 主查询使用PageHelper物理分页 -- select idselectOrders resultMaporderWithItemsMap SELECT id, order_no, create_time FROM orders WHERE status 1 /select !-- 子查询通过association/collection实现 -- resultMap idorderWithItemsMap typeOrder collection propertyitems columnid selectselectItemsByOrderId fetchTypelazy/ /resultMap select idselectItemsByOrderId resultTypeOrderItem SELECT * FROM order_items WHERE order_id #{id} /select2.3 方案三二次查询法大数据量优化当子查询数据量特别大时可采用批量查询优化// 先分页查询主表ID PageHelper.startPage(pageNum, pageSize); ListLong orderIds orderMapper.selectOrderIds(); // 批量查询关联数据 MapLong, ListOrderItem itemMap orderItemMapper .selectByOrderIds(orderIds) .stream() .collect(Collectors.groupingBy(OrderItem::getOrderId)); // 组装最终结果 ListOrder orders orderMapper.selectByIds(orderIds); orders.forEach(o - o.setItems(itemMap.get(o.getId())));3. 完整实现示例与避坑指南3.1 MyBatis配置关键点!-- 必须配置rewriteBatchedStatementstrue -- property nameurl valuejdbc:mysql://...?rewriteBatchedStatementstrue/ !-- PageHelper插件配置 -- plugins plugin interceptorcom.github.pagehelper.PageInterceptor property namehelperDialect valuemysql/ property namereasonable valuetrue/ /plugin /plugins3.2 典型错误场景复原错误示例1N1查询问题// 错误用法会导致N1查询 PageHelper.startPage(1, 10); ListOrder orders orderMapper.selectAllWithItems();注意虽然PageHelper在方法内有效但N1查询会严重拖慢系统错误示例2分页失效// 错误用法分页被后续查询覆盖 PageHelper.startPage(1, 10); orderService.someOtherQuery(); // 内部也有数据库操作 ListOrder orders orderMapper.selectAll(); // 分页已失效3.3 性能优化参数// 推荐设置保证分页线程安全 PageHelper.startPage(1, 10, true, false, true); // 参数说明 // 1. pageNum // 2. pageSize // 3. count是否执行count查询 // 4. reasonable分页合理化 // 5. pageSizeZero当pageSize0时返回全部结果4. 高级应用场景解决方案4.1 多级关联查询对于订单→订单项→商品的三级关联resultMap idorderWithDetailsMap typeOrder collection propertyitems columnid selectselectItems fetchTypelazy/ /resultMap resultMap iditemWithProductMap typeOrderItem association propertyproduct columnproduct_id selectselectProduct fetchTypelazy/ /resultMap4.2 分页排序复合需求// 使用PageHelper的orderBy方法 PageHelper.startPage(1, 10) .orderBy(create_time DESC); ListOrder orders orderMapper.selectAll();4.3 自定义Count查询对于复杂查询可指定count语句PageHelper.startPage(1, 10) .setCountSql(SELECT COUNT(1) FROM orders WHERE status1);5. 生产环境监控建议SQL监控通过Druid等工具监控子查询执行情况分页深度保护限制最大页码和每页条数// 在拦截器中添加参数校验 if(pageNum 100 || pageSize 100){ throw new IllegalArgumentException(分页参数过大); }缓存策略对高频访问的分页结果使用Redis缓存我在实际项目中发现当分页深度超过100页时90%的用户都不会继续浏览。建议结合业务特点设计合理的分页策略比如电商场景可以按分类、时间等维度拆分查询。6. 特殊数据库适配方案6.1 Oracle分页实现!-- 使用ROWNUM实现 -- select idselectOrders resultMap... SELECT * FROM ( SELECT a.*, ROWNUM rn FROM ( SELECT id, order_no FROM orders ORDER BY create_time DESC ) a WHERE ROWNUM #{end} ) WHERE rn #{start} /select6.2 SQL Server分页优化-- 使用OFFSET-FETCH语法 SELECT * FROM orders ORDER BY create_time DESC OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY7. 前沿技术拓展对于Spring Data JPA用户可以结合Hibernate的BatchSize注解优化Entity public class Order { BatchSize(size 100) OneToMany(mappedBy order) private ListOrderItem items; }这种方案在分页查询时会智能生成批量子查询比MyBatis的N1查询性能更好。不过需要特别注意二级缓存配置否则可能出现脏读问题。