You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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查询,又不会产生笛卡尔积,数据库层面完成分页,彻底消除内存分页警告。

  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();
    }
}
  1. 服务层直接使用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,同时批量加载关联集合。

  1. 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);
}
  1. 服务层实现批次查询与分页封装
    将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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 06:35:20