Search code examples
javarestspring-data-jpamany-to-manycomposite-primary-key

Spring JPA Many To Many with extra colum and Embedded ID


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;
    }

Solution

  • 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.