I have a model that has many-to-many relations with two other entities multiple times and when I try to save them I get a stack overflow error.
My relation is Profile with multiple Stat and Interest, so a Profile can have multiple Stats and a Stat can be in multiple Profiles, so a Profile can have multiple Interests and an Interest can be in multiple Profiles. Classes ProfileStat and ProfileInterest have the ProfileStatId and ProfileInterestId as IDClass respectively:
@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;
}
I have an endpoint that saves the profile without the Stats and Interests, but when I try to save the Stats and Interests it gives me an stack overflow exception. I save the profiles and create the Stats and Interests instances, but it can't finish creating the objects, let alone save them. The method that creates those objects:
public Profile enrichProfile(Profile profile, ProfileDTO profileDTO) throws RestRequestException {
// There's some code here that works, the problem is with the next line
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;
}
How I'm calling the method that creates the Stats and Interests, right after executing a repository.save()
on the Profile entity:
@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;
}
I solved this using this. I built a Github repository with a simple example.
My method that populates the object saves it but with a different structure:
private Set<ProfileStat> buildProfileStat(Collection<Stat> stats, Profile profile) {
Set<ProfileStat> profileStats = new HashSet<>();
for(Stat stat : stats) {
ProfileStatId profileStatId = new ProfileStatId(stat.getId(), profile.getId());
ProfileStat profileStat = new ProfileStat(profileStatId, fals e);
profileStat.setProfile(profile);
profileStat.setStat(stat);
profileStats.add(profileStatService.save(profileStat));
}
return profileStats;
}
private Set<ProfileInterest> buildProfileInterest(Collection<Interest> interests, Profile profile) {
Set<ProfileInterest> profileInterests = new HashSet<>();
for(Interest interest : interests) {
ProfileInterestId profileInterestId = new ProfileInterestId(interest.getId(), profile.getId());
ProfileInterest profileInterest = new ProfileInterest(profileInterestId, false);
profileInterest.setProfile(profile);
profileInterest.setInterest(interest);
profileInterests.add(profileInterestService.save(profileInterest));
}
return profileInterests;
}
I had to add a constructor on the ProfileStat
entity that accepts the ID class and the extra columns but doesn't need the objects; you'll add them later. I don't know if you can add them.
I also had to change how I populate back the "Profile", from
profile.setLanguages(buildProfileStat(findStats(profileDTO.getLanguages()), profile));
to buildProfileStat(findStats(profileDTO.getLanguages()), profile);
.
I couldn't set back the saved list to its main object instance, Profile; it throws a Stackoverflow Exception. Probably there's some workaround.