Skip to content

Commit 17a9235

Browse files
committed
DATAGRAPH-1391 - Temporarily save objects in creation.
This avoids the problem that an object in creation can get created multiple times if there is a cycle or similar in the graph.
1 parent 0d7b52c commit 17a9235

File tree

1 file changed

+58
-46
lines changed

1 file changed

+58
-46
lines changed

src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -214,33 +214,52 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
214214
private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescription, KnownObjects knownObjects,
215215
@Nullable Object lastMappedEntity) {
216216

217-
List<String> allLabels = getLabels(queryResult);
218-
NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
219-
.deriveConcreteNodeDescription(nodeDescription, allLabels);
220-
Neo4jPersistentEntity<ET> concreteNodeDescription = (Neo4jPersistentEntity<ET>) nodeDescriptionAndLabels
221-
.getNodeDescription();
222-
223-
Collection<RelationshipDescription> relationships = concreteNodeDescription.getRelationships();
224-
225-
ET instance = instantiate(concreteNodeDescription, queryResult, knownObjects, relationships,
226-
nodeDescriptionAndLabels.getDynamicLabels());
227-
228-
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(instance);
229-
230-
if (concreteNodeDescription.requiresPropertyPopulation()) {
231-
232-
// Fill simple properties
233-
Predicate<Neo4jPersistentProperty> isConstructorParameter = concreteNodeDescription
234-
.getPersistenceConstructor()::isConstructorParameter;
235-
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(queryResult, knownObjects, propertyAccessor,
236-
isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity);
237-
concreteNodeDescription.doWithProperties(handler);
217+
// if the given result does not contain an identifier to the mapped object cannot get temporarily saved
218+
Long internalId = queryResult.get(Constants.NAME_OF_INTERNAL_ID).isNull()
219+
? null
220+
: queryResult instanceof Node
221+
? ((Node) queryResult).id()
222+
: queryResult.get(Constants.NAME_OF_INTERNAL_ID).asLong();
223+
224+
Supplier<Object> mappedObjectSupplier = () -> {
225+
226+
List<String> allLabels = getLabels(queryResult);
227+
NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
228+
.deriveConcreteNodeDescription(nodeDescription, allLabels);
229+
Neo4jPersistentEntity<ET> concreteNodeDescription = (Neo4jPersistentEntity<ET>) nodeDescriptionAndLabels
230+
.getNodeDescription();
231+
232+
Collection<RelationshipDescription> relationships = concreteNodeDescription.getRelationships();
233+
234+
ET instance = instantiate(concreteNodeDescription, queryResult, knownObjects, relationships,
235+
nodeDescriptionAndLabels.getDynamicLabels());
236+
237+
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(instance);
238+
239+
if (concreteNodeDescription.requiresPropertyPopulation()) {
240+
241+
// Fill simple properties
242+
Predicate<Neo4jPersistentProperty> isConstructorParameter = concreteNodeDescription
243+
.getPersistenceConstructor()::isConstructorParameter;
244+
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(queryResult, propertyAccessor,
245+
isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity);
246+
concreteNodeDescription.doWithProperties(handler);
247+
248+
// in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
249+
// reference the start again. Because it is getting still constructed, it won't be in the knownObjects
250+
// store unless we temporarily put it there.
251+
knownObjects.storeObject(internalId, instance);
252+
// Fill associations
253+
concreteNodeDescription.doWithAssociations(
254+
populateFrom(queryResult, propertyAccessor, isConstructorParameter, relationships, knownObjects));
255+
}
256+
ET bean = propertyAccessor.getBean();
238257

239-
// Fill associations
240-
concreteNodeDescription.doWithAssociations(
241-
populateFrom(queryResult, propertyAccessor, isConstructorParameter, relationships, knownObjects));
242-
}
243-
return propertyAccessor.getBean();
258+
// save final state of the bean
259+
knownObjects.storeObject(internalId, bean);
260+
return bean;
261+
};
262+
return (ET) knownObjects.getObject(internalId).orElseGet(mappedObjectSupplier);
244263
}
245264

246265
/**
@@ -283,7 +302,7 @@ public Object getParameterValue(PreferredConstructor.Parameter parameter) {
283302
return INSTANTIATORS.getInstantiatorFor(nodeDescription).createInstance(nodeDescription, parameterValueProvider);
284303
}
285304

286-
private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult, KnownObjects knownObjects,
305+
private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult,
287306
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
288307
Collection<String> surplusLabels, Object targetNode) {
289308
return property -> {
@@ -398,7 +417,7 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
398417

399418
for (Relationship possibleRelationship : allMatchingTypeRelationshipsInResult) {
400419
if (targetIdSelector.apply(possibleRelationship) == nodeId) {
401-
Object mappedObject = knownObjects.computeIfAbsent(nodeId, () -> map(possibleValueNode, concreteTargetNodeDescription, knownObjects));
420+
Object mappedObject = map(possibleValueNode, concreteTargetNodeDescription, knownObjects);
402421
if (relationshipDescription.hasRelationshipProperties()) {
403422

404423
Object relationshipProperties = map(possibleRelationship,
@@ -417,10 +436,7 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
417436
} else {
418437
for (Value relatedEntity : list.asList(Function.identity())) {
419438

420-
Long internalIdValue = relatedEntity.get(Constants.NAME_OF_INTERNAL_ID).asLong();
421-
422-
Object valueEntry = knownObjects.computeIfAbsent(internalIdValue,
423-
() -> map(relatedEntity, concreteTargetNodeDescription, knownObjects));
439+
Object valueEntry = map(relatedEntity, concreteTargetNodeDescription, knownObjects);
424440

425441
if (relationshipDescription.hasRelationshipProperties()) {
426442
Relationship relatedEntityRelationship = relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP)
@@ -476,40 +492,36 @@ static class KnownObjects {
476492

477493
private final Map<Long, Object> internalIdStore = new HashMap<>();
478494

479-
Object computeIfAbsent(Long internalId, Supplier<Object> entitySupplier) {
480-
Object knownEntity = getObject(internalId);
481-
482-
// if it is not in the store, it has to get re-computed also for the internalIdStore
483-
if (knownEntity != null) {
484-
return knownEntity;
495+
private void storeObject(@Nullable Long internalId, Object object) {
496+
if (internalId == null) {
497+
return;
485498
}
486-
487499
try {
488500
write.lock();
489-
Object computedEntity = entitySupplier.get();
490-
internalIdStore.put(internalId, computedEntity);
491-
return computedEntity;
501+
internalIdStore.put(internalId, object);
492502
} finally {
493503
write.unlock();
494504
}
495505
}
496506

497-
@Nullable
498-
private Object getObject(Long internalId) {
507+
private Optional<Object> getObject(@Nullable Long internalId) {
508+
if (internalId == null) {
509+
return Optional.empty();
510+
}
499511
try {
500512

501513
read.lock();
502514

503515
Object knownEntity = internalIdStore.get(internalId);
504516

505517
if (knownEntity != null) {
506-
return knownEntity;
518+
return Optional.of(knownEntity);
507519
}
508520

509521
} finally {
510522
read.unlock();
511523
}
512-
return null;
524+
return Optional.empty();
513525
}
514526
}
515527
}

0 commit comments

Comments
 (0)