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
相关产品推荐
相关产品推荐

