Hibernate: why my relationship doesn’t update?
Hibernate is probably the most used JPA implementation but has sometimes some surprising behavior. One I hit recently was the relationship collection update. If you don't reuse the collection hibernate gives you it will not update the collection and consider it as a new one which can lead to weird result in the database depending your mapping.
This is quite frustrating when you map some web service (like a JSON payload in a JAX-RS endpoint) incoming data to your JPA model, typically this will not work:
json post(json) {
jpa = find(json);
jpa.setRelationship(json.getRelationship().map(this::map).collect(toList()));
return ...;
}
This is a common pattern in JSON->persistence layer but it recreates a collection for relationships and assumes the persistence layer will be able to persist the new properly. If hibernate already has a collection it needs this instance to do the "diff" correctly. If you replace it, in the best case it will just fail to persist the root entity but in the worse case it will persist unexpected results.
To solve that you need to ensure you never override an hibernate collection if already there.
Here is a helper function which makes it easier to map a Set (but it is easy to interpolate for List):
<JPA, JSON> Set<JPA> mapSet(Set<JPA> jpas, Collection<JSON> jsons,
Function<JPA, String> jpaGetId, Function<JSON, String> jsonGetId,
Function<JSON, JPA> mapper) {
// 1
final Set<JPA> aggregator = jpas == null ? new HashSet<>() : jpas;
final Collection<JSON> nullsafeJsons = ofNullable(jsons).orElse(emptySet());
// 2
final Map<String, JSON> idToKeep = nullsafeJsons.stream()
.filter(j -> jsonGetId.apply(j) != null)
.collect(toMap(jsonGetId, identity()));
// 3
aggregator.removeIf(jpa -> !idToKeep.containsKey(jpaGetId.apply(jpa)));
// 4
aggregator.forEach(j -> mapper.apply(idToKeep.get(jpaGetId.apply(j))));
// 5
aggregator.addAll(nullsafeJsons.stream().map(mapper).filter(j -> {
final String id = jpaGetId.apply(j);
return id == null || !idToKeep.containsKey(id);
}).collect(toSet()));
return aggregator;
}
The key points here are:
- get the list of the entities in the collection (aggregator).
- extract the identifiers of the entities in the set (from JSON payload). Note the usage of a Function makes it quite easy to avoid reflection (which would have been the java 7 implementation probably).
- remove from the JPA collection the entities no more in the set (based on the id)
- update all transitive entities (if there is a cascade update for instance)
- add all entities not already in the set to the collection
This code still assumes the mapper does a findOrCreate kind of logic. Here is an example:
private JPA map(JSON payload) {
final JPA model = ofNullable(payload.getId())
.map(id -> entityManager.find(JPA.class, id))
.orElseGet(() -> {
final JPA m = new JPA();
entityManager.persist(m);
return m;
});
model.setName(payload.getName());
model.setDescription(payload.getDescription());
return model;
}
One small optimization of this code - if you have huge collections - is to fetch all entities by id at onces with a contains instead of a simple find(). It would make sense if you can add relationship entities by batch.
There are surely more efficient ways to do the overall update but this one works not bad and is probably easy enough to understand the issue and the path to solve it.
In any case what this post you remind you is: never forget to ensure your mapping works as expected and your usage is consistent with the implementation you use.
If you are curious of why hibernate behaves this way you can have a look to org.hibernate.collection.internal.AbstractPersistentCollection which tracks the state and operations related to the relationship.
From the same author:
In the same category: