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

Spring JPA多对多含额外字段与嵌入式ID时保存触发栈溢出

问题描述

我开发的模型中,Profile实体与Stat、Interest两个实体分别为多对多关联:一个Profile可关联多个Stat,一个Stat也可被多个Profile关联;同时一个Profile可关联多个Interest,一个Interest也可被多个Profile关联。中间关联类ProfileStat和ProfileInterest分别使用ProfileStatId、ProfileInterestId作为嵌入式ID。

现有接口可保存未关联Stats和Interests的Profile,但尝试为已保存的Profile关联Stats和Interests时出现栈溢出异常。流程为先保存Profile,再创建关联实例,但连对象都无法创建,更无法完成保存。

实体类代码

@Entity
public class Profile extends GenericEntity {

    @Column(nullable = false, length = 100)
    private String name;

    @OneToOne(mappedBy = "stat", cascade = CascadeType.ALL, orphanRemoval = true)
    private ProfileStat smokes;

    @OneToMany(mappedBy = "stat", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<ProfileStat> languages;

    @OneToMany(mappedBy = "interest", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<ProfileInterest> relationshipInterests;
}

@Entity
public class ProfileStat {

    @EmbeddedId
    private ProfileStatId profileStatId = new ProfileStatId();

    @ManyToOne
    @MapsId("statId")
    @JoinColumn(name = "stat_id")
    private Stat stat;

    @ManyToOne
    @MapsId("profileId")
    @JoinColumn(name = "profile_id")
    private Profile profile;

    @Column
    private boolean displayable;
}

@Embeddable
public class ProfileStatId implements Serializable {

    private static final long serialVersionUID = 1L;

    private UUID statId;

    private UUID profileId;
}

@Entity
public class ProfileInterest {

    @EmbeddedId
    private ProfileInterestId profileInterestId = new ProfileInterestId();

    @ManyToOne
    @MapsId("interestId")
    @JoinColumn(name = "interest_id")
    private Interest interest;

    @ManyToOne
    @MapsId("profileId")
    @JoinColumn(name = "profile_id")
    private Profile profile;

    @Column
    private boolean displayable;
}

@Embeddable
public class ProfileInterestId implements Serializable {

    private static final long serialVersionUID = 1L;

    private UUID interestId;

    private UUID profileId;
}

创建关联对象的方法

public Profile enrichProfile(Profile profile, ProfileDTO profileDTO) throws RestRequestException {

    // 其他正常代码,问题出在下面两行
    profile.setLanguages(buildProfileStat(findStats(profileDTO.getLanguages()), profile));        
    profile.setRelationshipInterests(buildProfileInterest(findInterests(profileDTO.getRelationshipInterestsId()), profile));
    return profile;
}

private Set<ProfileStat> buildProfileStat(Collection<Stat> stats, Profile profile) {
    Set<ProfileStat> profileStats = new HashSet<>();
    for(Stat stat : stats) {
        profileStats.add(new ProfileStat(stat, profile, false));
    }
    return profileStats;
}

private Set<Stat> findStats(Collection<UUID> ids) throws RestRequestException {
    Set<Stat> stats = new HashSet<>();
    for (UUID id : ids) {
        stats.add(statService.findById(id)
                .orElseThrow(() -> new RestRequestException(HttpStatus.NOT_FOUND, ErrorIndicator.ERROR_CPRAPI_0012_D.getMessage())));
    }
    return stats;
}

private Set<ProfileInterest> buildProfileInterest(Collection<Interest> interests, Profile profile) {
    Set<ProfileInterest> profileInterest = new HashSet<>();
    for(Interest interest : interests) {
        profileInterest.add(new ProfileInterest(interest, profile, false));
    }
    return profileInterest;
}

private Set<Interest> findInterests(Collection<UUID> ids) throws RestRequestException {
    Set<Interest> interests = new HashSet<>();
    for (UUID id : ids) {
        interests.add(interestService.findById(id)
                .orElseThrow(() -> new RestRequestException(HttpStatus.NOT_FOUND, ErrorIndicator.ERROR_CPRAPI_0012_E.getMessage())));
    }
    return interests;
}

接口调用代码

@ApiOperation("Endpoint to register/save a new profile.")
@PostMapping(value = "/save")
public Profile saveProfile(@RequestBody ProfileDTO profileDTO) throws RestRequestException, CouplerPlanLimitationException {
    profileService.verifyUserCanSaveProfile(profileDTO.getUserId());
    Profile profile = profileService.save(profileFactory.newProfileDtoToProfile(profileDTO));
    profile = profileFactory.enrichProfile(profile, profileDTO);
    return profile;
}
问题分析

栈溢出的核心原因是Profile类中关联注解的mappedBy属性配置错误,导致Hibernate处理双向关联时陷入无限递归循环:

  • Profile的smokes字段@OneToOne(mappedBy = "stat"):mappedBy指向的应该是ProfileStat中维护关联的profile字段,而非stat
  • Profile的languages字段@OneToMany(mappedBy = "stat"):同样应该指向ProfileStat的profile字段
  • Profile的relationshipInterests字段@OneToMany(mappedBy = "interest"):应该指向ProfileInterest的profile字段

错误的mappedBy配置让Hibernate在解析关联时不断遍历无效的双向引用,最终触发栈溢出。

解决方案

1. 修正Profile类的关联映射

修改Profile中三个关联注解的mappedBy属性,指向关联类中的Profile字段:

@Entity
public class Profile extends GenericEntity {

    @Column(nullable = false, length = 100)
    private String name;

    @OneToOne(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true)
    private ProfileStat smokes;

    @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<ProfileStat> languages;

    @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<ProfileInterest> relationshipInterests;
}

2. 确保关联实例的双向绑定(可选但推荐)

创建关联实例时,确保关联类正确引用Profile,同时如果需要可以维护反向关联(当前Profile为被维护端,主要保证关联类的Profile属性正确即可):

private Set<ProfileStat> buildProfileStat(Collection<Stat> stats, Profile profile) {
    Set<ProfileStat> profileStats = new HashSet<>();
    for(Stat stat : stats) {
        ProfileStat profileStat = new ProfileStat(stat, profile, false);
        profileStats.add(profileStat);
    }
    return profileStats;
}

3. 保存更新后的Profile

修正映射后,关联对象可正常创建,最后需要再次保存Profile,利用CascadeType.ALL级联保存关联实例:

@ApiOperation("Endpoint to register/save a new profile.")
@PostMapping(value = "/save")
public Profile saveProfile(@RequestBody ProfileDTO profileDTO) throws RestRequestException, CouplerPlanLimitationException {
    profileService.verifyUserCanSaveProfile(profileDTO.getUserId());
    Profile profile = profileService.save(profileFactory.newProfileDtoToProfile(profileDTO));
    profile = profileFactory.enrichProfile(profile, profileDTO);
    // 保存更新后的Profile,级联保存关联实例
    return profileService.save(profile);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 18:34:57