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

多对多关联元素重复问题:添加本地化后的原因及非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解决重复问题:

  1. JPQL查询添加DISTINCT
    在查询语句中明确声明去重,比如:

    @Query("SELECT DISTINCT p FROM Profile p JOIN FETCH p.experiences e JOIN FETCH e.localizations")
    List<Profile> findAllWithExperiences();
    

    强制Hibernate对查询结果进行去重处理。

  2. 配置@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;
    
  3. 使用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;
    

遗漏的配置检查

  1. 复合主键的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);
    }
    
  2. 实体的equals()和hashCode()
    ProfileExperience类需要基于主键或业务唯一字段实现这两个方法,确保Hibernate在处理集合时能正确判断对象是否重复。

  3. 关联的Fetch模式
    默认的FetchMode.JOIN会触发笛卡尔积查询,切换为FetchMode.SELECT或SUBSELECT可以避免一次性加载所有关联数据导致的重复。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 09:23:06