Skip to content

Commit 32bb4c1

Browse files
GH-2355 - Improve handling of non-distinct collections and already visited objects.
This commit started as a bunch of tests that should show which self-referential relationships with optimistic locking are supported. The bug in #2355 could be reproduced with the `saveAll` call: The underlying issue bieng that one or more items had been visited at least twice: The first time as related object, than as root. The next root of course still be on the old version. That has been fixed by keeping the state machine tracking visited items for the duration of the `saveAll` calls around. Thus an related object will not be updated twice. For all of this to work in the most efficient way possible, a `java.util.Set` should be used for related objects, along with a valid `equals/hashCode` pair. This is however neglected. By adding an additional check in the state machine falling back to ID extraction, we can improve the user experience a lot and remove that need in many cases. This commit fixes #2355.
1 parent 1cad4b9 commit 32bb4c1

15 files changed

+1615
-70
lines changed

src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java

+41-17
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, Obj
316316
@Override
317317
public <T> T save(T instance) {
318318

319-
return saveImpl(instance, Collections.emptyList());
319+
return saveImpl(instance, Collections.emptyList(), null);
320320
}
321321

322322
@Override
@@ -336,7 +336,7 @@ public <T, R> R saveAs(T instance, Class<R> resultType) {
336336
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), resultType,
337337
projectionFactory, neo4jMappingContext);
338338

339-
T savedInstance = saveImpl(instance, pps);
339+
T savedInstance = saveImpl(instance, pps, null);
340340
if (projectionInformation.isClosed()) {
341341
return projectionFactory.createProjection(resultType, savedInstance);
342342
}
@@ -348,7 +348,11 @@ public <T, R> R saveAs(T instance, Class<R> resultType) {
348348
this.findById(propertyAccessor.getProperty(idProperty), savedInstance.getClass()).get());
349349
}
350350

351-
private <T> T saveImpl(T instance, Collection<PropertyPath> includedProperties) {
351+
private <T> T saveImpl(T instance, @Nullable Collection<PropertyPath> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) {
352+
353+
if (stateMachine != null && stateMachine.hasProcessedValue(instance)) {
354+
return instance;
355+
}
352356

353357
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(instance.getClass());
354358
boolean isEntityNew = entityMetaData.isNew(instance);
@@ -393,9 +397,17 @@ private <T> T saveImpl(T instance, Collection<PropertyPath> includedProperties)
393397
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId);
394398
}
395399
TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode.get());
396-
processRelations(entityMetaData, instance, internalId, propertyAccessor, isEntityNew, includeProperty);
397400

398-
return propertyAccessor.getBean();
401+
if (stateMachine == null) {
402+
stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext, instance, internalId);
403+
}
404+
405+
stateMachine.markValueAsProcessed(instance, internalId);
406+
processRelations(entityMetaData, propertyAccessor, isEntityNew, stateMachine, includeProperty);
407+
408+
T bean = propertyAccessor.getBean();
409+
stateMachine.markValueAsProcessedAs(instance, bean);
410+
return bean;
399411
}
400412

401413
@SuppressWarnings("unchecked")
@@ -447,7 +459,8 @@ private <T> List<T> saveAllImpl(Iterable<T> instances, List<PropertyPath> includ
447459
|| entityMetaData.getDynamicLabelsProperty().isPresent()) {
448460
log.debug("Saving entities using single statements.");
449461

450-
return entities.stream().map(e -> saveImpl(e, includedProperties)).collect(Collectors.toList());
462+
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
463+
return entities.stream().map(e -> saveImpl(e, includedProperties, stateMachine)).collect(Collectors.toList());
451464
}
452465

453466
class Tuple3<T> {
@@ -486,7 +499,7 @@ class Tuple3<T> {
486499
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
487500
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
488501
Long internalId = idToInternalIdMapping.get(id);
489-
return processRelations(entityMetaData, t.originalInstance, internalId, propertyAccessor, t.wasNew, TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
502+
return this.<T>processRelations(entityMetaData, propertyAccessor, t.wasNew, new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.originalInstance, internalId), TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
490503
}).collect(Collectors.toList());
491504
}
492505

@@ -634,24 +647,34 @@ private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, @Nulla
634647
* Starts of processing of the relationships.
635648
*
636649
* @param neo4jPersistentEntity The description of the instance to save
637-
* @param originalInstance The original parent instance. It is paramount to pass in the original instance (prior
638-
* to generating the id and prior to eventually create new instances via the property accessor,
639-
* so that we can reliable stop traversing relationships.
640650
* @param parentPropertyAccessor The property accessor of the parent, to modify the relationships
641651
* @param isParentObjectNew A flag if the parent was new
652+
* @param stateMachine Initial state of entity processing
642653
* @param includeProperty A predicate telling to include a relationship property or not
654+
* @param <T> The type of the object being initially processed
655+
* @return The owner of the relations being processed
643656
*/
644-
private <T> T processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, T originalInstance, Long internalId,
645-
PersistentPropertyAccessor<?> parentPropertyAccessor,
646-
boolean isParentObjectNew, PropertyFilter includeProperty) {
657+
private <T> T processRelations(
658+
Neo4jPersistentEntity<?> neo4jPersistentEntity,
659+
PersistentPropertyAccessor<?> parentPropertyAccessor,
660+
boolean isParentObjectNew,
661+
NestedRelationshipProcessingStateMachine stateMachine,
662+
PropertyFilter includeProperty
663+
) {
647664

648665
PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(neo4jPersistentEntity.getUnderlyingClass());
649666
return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew,
650-
new NestedRelationshipProcessingStateMachine(originalInstance, internalId), includeProperty, startingPropertyPath);
667+
stateMachine, includeProperty, startingPropertyPath);
651668
}
652669

653-
private <T> T processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, PersistentPropertyAccessor<?> propertyAccessor,
654-
boolean isParentObjectNew, NestedRelationshipProcessingStateMachine stateMachine, PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath previousPath) {
670+
private <T> T processNestedRelations(
671+
Neo4jPersistentEntity<?> sourceEntity,
672+
PersistentPropertyAccessor<?> propertyAccessor,
673+
boolean isParentObjectNew,
674+
NestedRelationshipProcessingStateMachine stateMachine,
675+
PropertyFilter includeProperty,
676+
PropertyFilter.RelaxedPropertyPath previousPath
677+
) {
655678

656679
Object fromId = propertyAccessor.getProperty(sourceEntity.getRequiredIdProperty());
657680

@@ -908,12 +931,13 @@ <T, R> List<R> doSave(Iterable<R> instances, Class<T> domainType) {
908931
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
909932
projectionFactory, neo4jMappingContext);
910933

934+
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
911935
List<R> results = new ArrayList<>();
912936
for (R instance : instances) {
913937
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
914938
T domainObject = converter.convert(instance);
915939

916-
T savedEntity = saveImpl(domainObject, pps);
940+
T savedEntity = saveImpl(domainObject, pps, stateMachine);
917941

918942
@SuppressWarnings("unchecked")
919943
R convertedBack = (R) new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedEntity);

src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java

+34-19
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, Obj
300300
@Override
301301
public <T> Mono<T> save(T instance) {
302302

303-
return saveImpl(instance, Collections.emptyList());
303+
return saveImpl(instance, Collections.emptyList(), null);
304304
}
305305

306306
@Override
@@ -320,7 +320,7 @@ public <T, R> Mono<R> saveAs(T instance, Class<R> resultType) {
320320
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), resultType,
321321
projectionFactory, neo4jMappingContext);
322322

323-
Mono<T> savingPublisher = saveImpl(instance, pps);
323+
Mono<T> savingPublisher = saveImpl(instance, pps, null);
324324
if (projectionInformation.isClosed()) {
325325
return savingPublisher.map(savedInstance -> projectionFactory.createProjection(resultType, savedInstance));
326326
}
@@ -346,22 +346,35 @@ <T, R> Flux<R> doSave(Iterable<R> instances, Class<T> domainType) {
346346
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
347347
projectionFactory, neo4jMappingContext);
348348

349+
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
349350
return Flux.fromIterable(instances)
350351
.flatMap(instance -> {
351352
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
352353
T domainObject = converter.convert(instance);
353354

354355
@SuppressWarnings("unchecked")
355-
Mono<R> result = saveImpl(domainObject, pps)
356+
Mono<R> result = saveImpl(domainObject, pps, stateMachine)
356357
.map(savedEntity -> (R) new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedEntity));
357358
return result;
358359
});
359360
}
360361

361-
private <T> Mono<T> saveImpl(T instance, @Nullable Collection<PropertyPath> includedProperties) {
362+
private <T> Mono<T> saveImpl(T instance, @Nullable Collection<PropertyPath> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) {
363+
364+
if (stateMachine != null && stateMachine.hasProcessedValue(instance)) {
365+
return Mono.just(instance);
366+
}
362367

363368
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(instance.getClass());
364369
boolean isNewEntity = entityMetaData.isNew(instance);
370+
371+
NestedRelationshipProcessingStateMachine finalStateMachine;
372+
if (stateMachine == null) {
373+
finalStateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
374+
} else {
375+
finalStateMachine = stateMachine;
376+
}
377+
365378
return Mono.just(instance).flatMap(eventSupport::maybeCallBeforeBind)
366379
.flatMap(entityToBeSaved -> determineDynamicLabels(entityToBeSaved, entityMetaData)).flatMap(t -> {
367380
T entityToBeSaved = t.getT1();
@@ -400,8 +413,9 @@ private <T> Mono<T> saveImpl(T instance, @Nullable Collection<PropertyPath> incl
400413
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), newOrUpdatedNode.id());
401414
}
402415
TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode);
416+
finalStateMachine.markValueAsProcessed(instance, newOrUpdatedNode.id());
403417
}).map(Entity::id)
404-
.flatMap(internalId -> processRelations(entityMetaData, instance, internalId, propertyAccessor, isNewEntity, includeProperty));
418+
.flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, finalStateMachine, includeProperty));
405419
});
406420
}
407421

@@ -486,7 +500,8 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable List<PropertyPa
486500
|| entityMetaData.getDynamicLabelsProperty().isPresent()) {
487501
log.debug("Saving entities using single statements.");
488502

489-
return Flux.fromIterable(entities).flatMap(e -> this.saveImpl(e, includedProperties));
503+
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
504+
return Flux.fromIterable(entities).concatMap(e -> this.saveImpl(e, includedProperties, stateMachine));
490505
}
491506

492507
@SuppressWarnings("unchecked") // We can safely assume here that we have a homongous collection with only one single type being either T or extending it
@@ -517,10 +532,8 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable List<PropertyPa
517532
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
518533
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
519534
Long internalId = idToInternalIdMapping.get(id);
520-
return processRelations(entityMetaData, t.getT1(), internalId,
521-
propertyAccessor, t.getT2(),
522-
TemplateSupport.computeIncludePropertyPredicate(includedProperties,
523-
entityMetaData));
535+
return processRelations(entityMetaData, propertyAccessor, t.getT2(), new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.getT1(), internalId),
536+
TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
524537
}))
525538
);
526539
}
@@ -750,22 +763,24 @@ Publisher<Tuple2<Collection<Long>, Collection<Long>>>> iterateAndMapNextLevel(
750763
* Starts of processing of the relationships.
751764
*
752765
* @param neo4jPersistentEntity The description of the instance to save
753-
* @param originalInstance The original parent instance. It is paramount to pass in the original instance (prior
754-
* to generating the id and prior to eventually create new instances via the property accessor,
755-
* so that we can reliable stop traversing relationships.
756766
* @param parentPropertyAccessor The property accessor of the parent, to modify the relationships
757767
* @param isParentObjectNew A flag if the parent was new
768+
* @param stateMachine Initial state of entity processing
758769
* @param includeProperty A predicate telling to include a relationship property or not
759-
* @param <T> The type of the entity to save
760-
* @return A mono representing the whole stream of save operations.
770+
* @param <T> The type of the object being initially processed
771+
* @return A mono representing the whole stream of save operations, eventually containing the owner of the relations being processed
761772
*/
762-
private <T> Mono<T> processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, T originalInstance,
763-
Long internalId, PersistentPropertyAccessor<?> parentPropertyAccessor,
764-
boolean isParentObjectNew, PropertyFilter includeProperty) {
773+
private <T> Mono<T> processRelations(
774+
Neo4jPersistentEntity<?> neo4jPersistentEntity,
775+
PersistentPropertyAccessor<?> parentPropertyAccessor,
776+
boolean isParentObjectNew,
777+
NestedRelationshipProcessingStateMachine stateMachine,
778+
PropertyFilter includeProperty
779+
) {
765780

766781
PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(neo4jPersistentEntity.getUnderlyingClass());
767782
return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew,
768-
new NestedRelationshipProcessingStateMachine(originalInstance, internalId), includeProperty, startingPropertyPath);
783+
stateMachine, includeProperty, startingPropertyPath);
769784
}
770785

771786
private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, PersistentPropertyAccessor<?> parentPropertyAccessor,

0 commit comments

Comments
 (0)