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

