如何在OneToMany关联分页时避免Fetch的firstResult/maxResults内存分页警告
解决分页查询Customer关联Figures的Hibernate内存分页警告问题
问题核心
分页查询Customer时,需要同时加载关联的Figures集合,还要返回包含numberOfFigures(集合大小)的CustomerDTO,但遇到以下问题:
- 使用
fetch join分页触发HHH000104警告(Hibernate被迫在内存中分页) EntityGraph未解决问题- 先查ID再用IN子句会生成超大SQL,不适合大数据场景
可行解决方案
方案1:使用Hibernate的@Fetch(FetchMode.SUBSELECT)注解(推荐,简单高效)
利用Hibernate的子查询批量加载关联集合,既避免N+1查询,又不会产生笛卡尔积,数据库层面完成分页,彻底消除内存分页警告。
- 修改Customer实体的figures字段
添加@Fetch(FetchMode.SUBSELECT)注解:
public class Customer implements UserDetails, Serializable { // 其他字段... @OneToMany(mappedBy = "createdBy", cascade = {CascadeType.MERGE, CascadeType.PERSIST}, fetch = FetchType.LAZY, orphanRemoval = true) @ToString.Exclude @Fetch(FetchMode.SUBSELECT) // 新增注解 private Set<Figure> figures = new HashSet<>(); // numberOfCreatedFigures方法保持不变 public Integer numberOfCreatedFigures() { return figures.size(); } }
- 服务层直接使用Spring Data JPA分页查询
不需要修改查询逻辑,Spring Data JPA的分页会自动触发两次查询:
- 第一次:分页查询
Customer主数据(数据库层面分页) - 第二次:通过子查询批量加载当前页所有
Customer的Figures集合
@Service public class CustomerService { private final CustomerRepository customerRepository; public Page<Customer> listAll(Pageable pageable) { return customerRepository.findAll(pageable); } }
优点:代码侵入性低,无需手动处理批量查询,性能稳定,适合大部分场景。
缺点:依赖Hibernate特定注解,若后续更换JPA实现需要调整。
方案2:拆分ID批次的两次查询(无Hibernate依赖,适合大数据场景)
将分页获取的ID列表拆分成小批次,避免IN子句过长生成超大SQL,同时批量加载关联集合。
- Repository新增方法
public interface CustomerRepository extends JpaRepository<Customer, Long> { // 分页查询Customer ID @Query("select c.id from Customer c") Page<Long> findIdsBy(Pageable pageable); // 按ID批次查询并加载Figures @Query("select c from Customer c left join fetch c.figures where c.id in :ids") List<Customer> findByIdInWithFigures(@Param("ids") List<Long> ids); }
- 服务层实现批次查询与分页封装
将ID列表拆分为小批次(比如每50个一组):
@Service public class CustomerService { private final CustomerRepository customerRepository; public Page<Customer> listAll(Pageable pageable) { // 1. 分页获取当前页的Customer ID Page<Long> customerIdsPage = customerRepository.findIdsBy(pageable); List<Long> customerIds = customerIdsPage.getContent(); if (customerIds.isEmpty()) { return new PageImpl<>(Collections.emptyList(), pageable, 0); } // 2. 拆分ID为小批次(避免IN子句过长) List<List<Long>> idBatches = new ArrayList<>(); for (int i = 0; i < customerIds.size(); i += 50) { idBatches.add(customerIds.subList(i, Math.min(i + 50, customerIds.size()))); } // 3. 批量查询每个批次的Customer并加载Figures List<Customer> customers = idBatches.stream() .flatMap(batch -> customerRepository.findByIdInWithFigures(batch).stream()) .collect(Collectors.toList()); // 4. 保持分页信息,封装为Page对象 return new PageImpl<>(customers, pageable, customerIdsPage.getTotalElements()); } }
优点:不依赖Hibernate特定API,适合超大分页场景,避免SQL过长问题。
缺点:需要手动处理批次拆分和分页封装,代码量稍大。
验证DTO转换
两种方案都已提前加载Figures集合,ModelMapper转换CustomerDTO时,numberOfCreatedFigures()方法直接返回集合大小,不会触发懒加载或N+1查询,原有的ModelMapper配置无需修改:
@Bean public ModelMapper modelMapper() { ModelMapper modelMapper = new ModelMapper(); modelMapper.createTypeMap(Customer.class, CustomerDTO.class) .addMappings(mapper -> mapper .map(Customer::numberOfCreatedFigures, CustomerDTO::setNumberOfFigures)); return modelMapper; }
内容的提问来源于stack exchange,提问作者deja
相关产品推荐
相关产品推荐

