多对多关联元素重复问题:添加本地化后的原因及非Set解决方案
我在Profile与ProfileExperience之间配置了如下多对多关联:
@ManyToMany @JoinTable(name = "profile_experience_relations", joinColumns = { @JoinColumn(name = "profile_id") }, inverseJoinColumns = { @JoinColumn(name = "profile_experience_id") }) private List<ProfileExperience> experiences;
随后为ProfileExperience添加了本地化支持,相关代码如下:
ProfileExperience类的本地化关联代码
@OneToMany(mappedBy = "profileExperience", cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, orphanRemoval = true) @MapKey(name = "localizedProfileExperiencePk.locale") @Cache(usage = CacheConcurrencyStrategy.READ_ONLY) private Map<String, LocalizedProfileExperience> localizations = new HashMap<>();
LocalizedProfileExperience实体类
@Entity @Getter @Setter @Cache(usage = CacheConcurrencyStrategy.READ_ONLY) public class LocalizedProfileExperience { @EmbeddedId private LocalizedProfileExperiencePk localizedProfileExperiencePk; @ManyToOne @MapsId("id") @JoinColumn(name = "profileExperienceId") private ProfileExperience profileExperience; private String value; }
复合主键类LocalizedProfileExperiencePk
@Embeddable @Getter @Setter public class LocalizedProfileExperiencePk implements Serializable { private static final long serialVersionUID = 1L; private String profileExperienceId; private String locale; public LocalizedProfileExperiencePk() { } }
添加本地化前查询结果无重复,但现在所有数据都出现重复。使用Set替换List可解决该问题,想了解:
- 出现重复的原因
- 是否可不使用
Set解决该问题 - 是否存在遗漏的配置?
重复原因
Hibernate处理嵌套关联查询(多对多+一对多)时,默认会生成笛卡尔积形式的SQL结果集。当一个Profile关联多个ProfileExperience,每个ProfileExperience又对应多条本地化记录时,SQL会返回每条本地化记录对应的ProfileExperience副本。由于List允许重复元素,这些副本会被直接存入集合,最终表现为数据重复。
不使用Set的解决办法
有三种可靠方案可以替代Set解决重复问题:
JPQL查询添加
DISTINCT
在查询语句中明确声明去重,比如:@Query("SELECT DISTINCT p FROM Profile p JOIN FETCH p.experiences e JOIN FETCH e.localizations") List<Profile> findAllWithExperiences();强制Hibernate对查询结果进行去重处理。
配置
@Fetch(FetchMode.SUBSELECT)
在Profile的多对多关联上添加该注解,让Hibernate先查询所有Profile,再通过子查询批量获取关联的ProfileExperience,避免笛卡尔积的生成:@ManyToMany @JoinTable(name = "profile_experience_relations", joinColumns = {@JoinColumn(name = "profile_id")}, inverseJoinColumns = {@JoinColumn(name = "profile_experience_id")}) @Fetch(FetchMode.SUBSELECT) private List<ProfileExperience> experiences;使用Hibernate的
@Distinct注解
在List字段上添加该注解,Hibernate会自动在查询中插入DISTINCT并处理结果去重:@ManyToMany @JoinTable(name = "profile_experience_relations", joinColumns = {@JoinColumn(name = "profile_id")}, inverseJoinColumns = {@JoinColumn(name = "profile_experience_id")}) @org.hibernate.annotations.Distinct private List<ProfileExperience> experiences;
遗漏的配置检查
复合主键的
equals()和hashCode()
你的LocalizedProfileExperiencePk缺少这两个方法的实现,Hibernate无法正确识别重复的主键实例,可能间接导致关联数据重复。补充实现示例:@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LocalizedProfileExperiencePk that = (LocalizedProfileExperiencePk) o; return Objects.equals(profileExperienceId, that.profileExperienceId) && Objects.equals(locale, that.locale); } @Override public int hashCode() { return Objects.hash(profileExperienceId, locale); }实体的
equals()和hashCode()ProfileExperience类需要基于主键或业务唯一字段实现这两个方法,确保Hibernate在处理集合时能正确判断对象是否重复。关联的Fetch模式
默认的FetchMode.JOIN会触发笛卡尔积查询,切换为FetchMode.SELECT或SUBSELECT可以避免一次性加载所有关联数据导致的重复。
内容的提问来源于stack exchange,提问作者benjaminv2

