Hibernate: duplicate key value violates unique constraint on collection
I want to import Profiles, that have a set of Professions, and a set of ProfessionGroups. The Professions are distributed accross the ProfessionGroups.
Following Entities are defined:
@Entity
public class Profile extends BaseEntity<Profile> { // B.E. defines id, creation_time,etc..
@OneToMany(cascade = CascadeType.ALL)
private Collection<Profession> professions;
@OneToMany(cascade = CascadeType.ALL)
private Collection<ProfessionGroup> professionGroups;
// .. getters and setters
}
@Entity
private class Profession extends BaseEntity<Profession> {
@Column(unique = true)
private String name;
// getters and setters
}
@Entity
public class ProfessionGroup extends BaseEntity<ProfessionGroup> {
@Column(unique = true)
private String name;
@ManyToOne(cascade = CascadeType.All)
private Collection<Profession> professions;
// getters and setters
}
Following code reads in some profiles serialized as json and wants to store it into the database:
// ...
Profile p = ...; // read from json using some deserializer
p.getProfessionGroups().forEach(pg -> pg.setProfessions(p.getProfessions());
// ..
ProfileService profileService = ...; //
profileService.save(profile);
The ProfileService calls internally entityManager.persist(...). The problem here is, that I get an "duplicate key value violates unique constraint" whenever i want to distribute all professions to all professionGroups. what can i do to safely store the profile, without getting a unique key constraint violation. JPA obviously wants to create a new professions for each entry in the professiongroups. However, the reference(s) to the profession(s) are the same. calling merge(...) didn't do the trick.
The problem lies in the definition of cascades, and how JPA and in particular hibernate handles a new entity instance.
When entitymanager.persist
is called, it stores and manages the state of the entity, but not the actual object you pass to the entityManager.persist
.
The managed instance and the parameter passed will be different. Hence if you generate ids manually, calling entitymanager.persist
twice with the same object will result into DuplicateKeyException
from the database, not from jpa.
To get way a round of this, you need to persist and get reference to the managed entities of the Profession instances, which you can use both in the Profile and in the ProfessionGroup, thus:
Profile profile = loadProfiles();
List<Profession> managedProfessions = profile
.getProfessions()
.stream()
.map((p) -> entityManager.merge(p)) //Note that we use the returned value, since the returned value is what is actually managed, the passed parameter is not, and will be discarded by the persistent-context
.Collect(Collectors.toList());
profile.setProfessions(managedProfessions);
profile.getProfessionGroups().forEach((gr)->gr.setProfessions(managedProfessions));
profileService.save(profile);
With this, you may want to remove the Cascade.ALL
and only replace it with Cascade.MERGE
.
the solution can be found sometimes somewhere else:
the relationship between the entities 'profession' and 'professiongroup' is wrong. it should have been:
@Entity
public class ProfessionGroup extends BaseEntity<ProfessionGroup> {
@Column(unique = true)
private String name;
@ManyToMany(cascade = CascadeType.All)
private Collection<Profession> professions;
// getters and setters
// ..
}
a profession can be in one or more profession-groups, as a profession-group could have one or more profession. having this fixed, and adapting the valuable answer from @maress the solution now looks like this:
Profile profile = mapper.readValue(json, Profile.class);
List<Profession> managedProfessions = profile
.getProfessions()
.stream()
.map(p -> {
return professionService.update(p);
})
.map(Optional::get)
.collect(Collectors.toList());
profile.setProfessions(managedProfessions);
profile.getProfessiongroups().forEach(professionGroup -> {
professionGroup.setProfessions(managedProfessions);
});
profileService.save(profile);
piece of cake.