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

Hibernate CriteriaQuery一对多关联查询返回重复记录问题

解决Hibernate一对多关联查询重复记录问题

概述

维护遗留代码时,从Documento实体加载所有子实体的流程整体正常,但revisoes一对多关联出现重复记录,使用distinct关键字未能解决该问题。

问题描述

Documento类包含多个子实体关联,其中仅revisoes为一对多关联,其余均为多对多关联。加载全部实体时,revisoes集合返回重复记录,需找到有效方案避免重复。

代码示例

实体类代码

@Entity
@Table(name = "documentos")
public class Documento extends Model {

    @NotFound(action = NotFoundAction.IGNORE)
    @ManyToOne(fetch = FetchType.EAGER)
    private TipoDeDocumento tipoDeDocumento;

    @Column(length = 250, nullable = false, unique = true)
    private String codigo;

    @NotFound(action = NotFoundAction.IGNORE)
    @ManyToMany(fetch = FetchType.EAGER)
    private Set<QuesitosNormas> quesitosNormas;

    @ManyToMany(fetch = FetchType.LAZY)
    private Set<PontoDeDistribuicao> pontosDistribuicao;

    @ManyToMany(fetch = FetchType.LAZY)
    private Set<Processo> processos;

    @Cascade(org.hibernate.annotations.CascadeType.ALL)
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "documento")
    private Collection<DocumentoRevisao> revisoes;
}

查询代码

@Override
public Documento find(Long id, Session session, boolean closeSession) {
    try {
        CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
        
        CriteriaQuery<Documento> criteriaQuery = criteriaBuilder.createQuery(Documento.class);
      
        Root<Documento> rootDocument = criteriaQuery.from(Documento.class);
        rootDocument.fetch("quesitosNormas", JoinType.LEFT); // 多对多关联
        rootDocument.fetch("pontosDistribuicao", JoinType.LEFT); // 多对多关联
        rootDocument.fetch("processos", JoinType.LEFT); // 多对多关联
        rootDocument.fetch("tipoDeDocumento",JoinType.LEFT); // 一对一关联
        rootDocument.fetch("revisoes", JoinType.LEFT); // 一对多关联
        
        criteriaQuery.select(rootDocument);
        criteriaQuery.where(criteriaBuilder.equal(rootDocument.get("id"), id));
        criteriaQuery.distinct(true);

        Query<Documento> query = session.createQuery(criteriaQuery);
       
        Documento singleResult = query.getSingleResult();

        return singleResult;

    } catch (Exception ex) {
        throw ex;
    } finally {
        if (session != null && closeSession) {
            session.close();
        }
    }
}

解决方案

1. 将集合类型改为Set并实现equals/hashCode

把revisoes的Collection类型替换为Set,利用Set自动去重的特性:

@Cascade(org.hibernate.annotations.CascadeType.ALL)
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "documento")
private Set<DocumentoRevisao> revisoes;

同时确保DocumentoRevisao类正确实现equals()和hashCode()方法,基于实体的唯一标识(如主键ID)编写逻辑,保证重复实例能被识别。

2. 使用Hibernate结果转换器

通过DistinctRootEntityResultTransformer对结果去重,需将JPA Query转换为Hibernate Query后设置转换器:

Query<Documento> query = session.createQuery(criteriaQuery);
// 转换为Hibernate原生Query
org.hibernate.query.Query<Documento> hibernateQuery = query.unwrap(org.hibernate.query.Query.class);
hibernateQuery.setResultTransformer(org.hibernate.transform.DistinctRootEntityResultTransformer.INSTANCE);
Documento singleResult = hibernateQuery.getSingleResult();

该转换器会确保根实体唯一,并清理关联集合中的重复元素。

3. 避免多集合同时Fetch导致笛卡尔积

当前查询同时Fetch多个集合关联(多对多+一对多),会产生笛卡尔积,导致集合元素重复。可通过以下方式优化:

  • 减少即时Fetch的集合数量,保留必要的关联Fetch,其余依赖懒加载自动初始化;
  • 分两次查询:先获取Documento实体,再单独初始化revisoes集合:
Documento singleResult = query.getSingleResult();
// 手动初始化懒加载集合
org.hibernate.Hibernate.initialize(singleResult.getRevisoes());
  • 使用JPA Entity Graph精确控制加载的关联,避免不必要的集合关联同时加载。

4. 调整JPQL查询逻辑

改用JPQL查询时,使用DISTINCT结合FETCH JOIN,但注意不要同时Fetch多个集合:

SELECT DISTINCT d FROM Documento d
LEFT JOIN FETCH d.tipoDeDocumento
LEFT JOIN FETCH d.quesitosNormas
LEFT JOIN FETCH d.revisoes
WHERE d.id = :id

配合实体类中revisoes为Set类型,可有效避免重复。

内容的提问来源于stack exchange,提问作者KenobiBastila

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 01:48:15