Skip to content

Commit 1021aa3

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 f825e14 commit 1021aa3

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.getPersistentEntity(instance.getClass());
354358
boolean isEntityNew = entityMetaData.isNew(instance);
@@ -391,9 +395,17 @@ private <T> T saveImpl(T instance, Collection<PropertyPath> includedProperties)
391395
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId);
392396
}
393397
TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode.get());
394-
processRelations(entityMetaData, instance, internalId, propertyAccessor, isEntityNew, includeProperty);
395398

396-
return propertyAccessor.getBean();
399+
if (stateMachine == null) {
400+
stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext, instance, internalId);
401+
}
402+
403+
stateMachine.markValueAsProcessed(instance, internalId);
404+
processRelations(entityMetaData, propertyAccessor, isEntityNew, stateMachine, includeProperty);
405+
406+
T bean = propertyAccessor.getBean();
407+
stateMachine.markValueAsProcessedAs(instance, bean);
408+
return bean;
397409
}
398410

399411
private <T> DynamicLabels determineDynamicLabels(T entityToBeSaved, Neo4jPersistentEntity<?> entityMetaData) {
@@ -444,7 +456,8 @@ private <T> List<T> saveAllImpl(Iterable<T> instances, List<PropertyPath> includ
444456
|| entityMetaData.getDynamicLabelsProperty().isPresent()) {
445457
log.debug("Saving entities using single statements.");
446458

447-
return entities.stream().map(e -> saveImpl(e, includedProperties)).collect(Collectors.toList());
459+
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
460+
return entities.stream().map(e -> saveImpl(e, includedProperties, stateMachine)).collect(Collectors.toList());
448461
}
449462

450463
class Tuple3<T> {
@@ -482,7 +495,7 @@ class Tuple3<T> {
482495
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
483496
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
484497
Long internalId = idToInternalIdMapping.get(id);
485-
return processRelations(entityMetaData, t.originalInstance, internalId, propertyAccessor, t.wasNew, TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
498+
return this.<T>processRelations(entityMetaData, propertyAccessor, t.wasNew, new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.originalInstance, internalId), TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
486499
}).collect(Collectors.toList());
487500
}
488501

@@ -628,24 +641,34 @@ private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, @Nulla
628641
* Starts of processing of the relationships.
629642
*
630643
* @param neo4jPersistentEntity The description of the instance to save
631-
* @param originalInstance The original parent instance. It is paramount to pass in the original instance (prior
632-
* to generating the id and prior to eventually create new instances via the property accessor,
633-
* so that we can reliable stop traversing relationships.
634644
* @param parentPropertyAccessor The property accessor of the parent, to modify the relationships
635645
* @param isParentObjectNew A flag if the parent was new
646+
* @param stateMachine Initial state of entity processing
636647
* @param includeProperty A predicate telling to include a relationship property or not
648+
* @param <T> The type of the object being initially processed
649+
* @return The owner of the relations being processed
637650
*/
638-
private <T> T processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, T originalInstance, Long internalId,
639-
PersistentPropertyAccessor<?> parentPropertyAccessor,
640-
boolean isParentObjectNew, PropertyFilter includeProperty) {
651+
private <T> T processRelations(
652+
Neo4jPersistentEntity<?> neo4jPersistentEntity,
653+
PersistentPropertyAccessor<?> parentPropertyAccessor,
654+
boolean isParentObjectNew,
655+
NestedRelationshipProcessingStateMachine stateMachine,
656+
PropertyFilter includeProperty
657+
) {
641658

642659
PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(neo4jPersistentEntity.getUnderlyingClass());
643660
return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew,
644-
new NestedRelationshipProcessingStateMachine(originalInstance, internalId), includeProperty, startingPropertyPath);
661+
stateMachine, includeProperty, startingPropertyPath);
645662
}
646663

647-
private <T> T processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, PersistentPropertyAccessor<?> propertyAccessor,
648-
boolean isParentObjectNew, NestedRelationshipProcessingStateMachine stateMachine, PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath previousPath) {
664+
private <T> T processNestedRelations(
665+
Neo4jPersistentEntity<?> sourceEntity,
666+
PersistentPropertyAccessor<?> propertyAccessor,
667+
boolean isParentObjectNew,
668+
NestedRelationshipProcessingStateMachine stateMachine,
669+
PropertyFilter includeProperty,
670+
PropertyFilter.RelaxedPropertyPath previousPath
671+
) {
649672

650673
Object fromId = propertyAccessor.getProperty(sourceEntity.getRequiredIdProperty());
651674

@@ -895,12 +918,13 @@ <T, R> List<R> doSave(Iterable<R> instances, Class<T> domainType) {
895918
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
896919
projectionFactory, neo4jMappingContext);
897920

921+
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
898922
List<R> results = new ArrayList<>();
899923
for (R instance : instances) {
900924
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
901925
T domainObject = converter.convert(instance);
902926

903-
T savedEntity = saveImpl(domainObject, pps);
927+
T savedEntity = saveImpl(domainObject, pps, stateMachine);
904928

905929
R convertedBack = (R) new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedEntity);
906930
results.add(convertedBack);

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

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

302-
return saveImpl(instance, Collections.emptyList());
302+
return saveImpl(instance, Collections.emptyList(), null);
303303
}
304304

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

322-
Mono<T> savingPublisher = saveImpl(instance, pps);
322+
Mono<T> savingPublisher = saveImpl(instance, pps, null);
323323
if (projectionInformation.isClosed()) {
324324
return savingPublisher.map(savedInstance -> projectionFactory.createProjection(resultType, savedInstance));
325325
}
@@ -345,20 +345,33 @@ <T, R> Flux<R> doSave(Iterable<R> instances, Class<T> domainType) {
345345
Collection<PropertyPath> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
346346
projectionFactory, neo4jMappingContext);
347347

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

353-
return saveImpl(domainObject, pps)
354+
return saveImpl(domainObject, pps, stateMachine)
354355
.map(savedEntity -> (R) new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedEntity));
355356
});
356357
}
357358

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

360365
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getPersistentEntity(instance.getClass());
361366
boolean isNewEntity = entityMetaData.isNew(instance);
367+
368+
NestedRelationshipProcessingStateMachine finalStateMachine;
369+
if (stateMachine == null) {
370+
finalStateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
371+
} else {
372+
finalStateMachine = stateMachine;
373+
}
374+
362375
return Mono.just(instance).flatMap(eventSupport::maybeCallBeforeBind)
363376
.flatMap(entityToBeSaved -> determineDynamicLabels(entityToBeSaved, entityMetaData)).flatMap(t -> {
364377
T entityToBeSaved = t.getT1();
@@ -395,8 +408,9 @@ private <T> Mono<T> saveImpl(T instance, @Nullable Collection<PropertyPath> incl
395408
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), newOrUpdatedNode.id());
396409
}
397410
TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode);
411+
finalStateMachine.markValueAsProcessed(instance, newOrUpdatedNode.id());
398412
}).map(Entity::id)
399-
.flatMap(internalId -> processRelations(entityMetaData, instance, internalId, propertyAccessor, isNewEntity, includeProperty));
413+
.flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, finalStateMachine, includeProperty));
400414
});
401415
}
402416

@@ -480,7 +494,8 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable List<PropertyPa
480494
|| entityMetaData.getDynamicLabelsProperty().isPresent()) {
481495
log.debug("Saving entities using single statements.");
482496

483-
return Flux.fromIterable(entities).flatMap(e -> this.saveImpl(e, includedProperties));
497+
NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
498+
return Flux.fromIterable(entities).concatMap(e -> this.saveImpl(e, includedProperties, stateMachine));
484499
}
485500

486501
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass);
@@ -510,10 +525,8 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable List<PropertyPa
510525
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
511526
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
512527
Long internalId = idToInternalIdMapping.get(id);
513-
return processRelations(entityMetaData, t.getT1(), internalId,
514-
propertyAccessor, t.getT2(),
515-
TemplateSupport.computeIncludePropertyPredicate(includedProperties,
516-
entityMetaData));
528+
return processRelations(entityMetaData, propertyAccessor, t.getT2(), new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.getT1(), internalId),
529+
TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
517530
}))
518531
);
519532
}
@@ -728,22 +741,24 @@ Publisher<Tuple2<Collection<Long>, Collection<Long>>>> iterateAndMapNextLevel(
728741
* Starts of processing of the relationships.
729742
*
730743
* @param neo4jPersistentEntity The description of the instance to save
731-
* @param originalInstance The original parent instance. It is paramount to pass in the original instance (prior
732-
* to generating the id and prior to eventually create new instances via the property accessor,
733-
* so that we can reliable stop traversing relationships.
734744
* @param parentPropertyAccessor The property accessor of the parent, to modify the relationships
735745
* @param isParentObjectNew A flag if the parent was new
746+
* @param stateMachine Initial state of entity processing
736747
* @param includeProperty A predicate telling to include a relationship property or not
737-
* @param <T> The type of the entity to save
738-
* @return A mono representing the whole stream of save operations.
748+
* @param <T> The type of the object being initially processed
749+
* @return A mono representing the whole stream of save operations, eventually containing the owner of the relations being processed
739750
*/
740-
private <T> Mono<T> processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, T originalInstance,
741-
Long internalId, PersistentPropertyAccessor<?> parentPropertyAccessor,
742-
boolean isParentObjectNew, PropertyFilter includeProperty) {
751+
private <T> Mono<T> processRelations(
752+
Neo4jPersistentEntity<?> neo4jPersistentEntity,
753+
PersistentPropertyAccessor<?> parentPropertyAccessor,
754+
boolean isParentObjectNew,
755+
NestedRelationshipProcessingStateMachine stateMachine,
756+
PropertyFilter includeProperty
757+
) {
743758

744759
PropertyFilter.RelaxedPropertyPath startingPropertyPath = PropertyFilter.RelaxedPropertyPath.withRootType(neo4jPersistentEntity.getUnderlyingClass());
745760
return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew,
746-
new NestedRelationshipProcessingStateMachine(originalInstance, internalId), includeProperty, startingPropertyPath);
761+
stateMachine, includeProperty, startingPropertyPath);
747762
}
748763

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

0 commit comments

Comments
 (0)