Skip to content

Commit 75c7f12

Browse files
committed
GH-2223 - Consider created objects as processed.
onSave immutable entities will get re-created if they have generated ids. In those cases if a node gets referenced more than once (e.g. A->B->C and A->C) the resulting entities will not be considered as the same because one has already an identifier set, the other not yet. In thoses cases the logic either persisted a duplicate or it could not find the already processed entity because the identifier of the entity in question is still `null`. Closes #2223
1 parent cf0e8d7 commit 75c7f12

30 files changed

+4240
-292
lines changed

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

+71-35
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,10 @@ private <T> T saveImpl(T instance, @Nullable String inDatabase) {
251251
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
252252
if (entityMetaData.isUsingInternalIds()) {
253253
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), optionalInternalId.get());
254-
entityToBeSaved = propertyAccessor.getBean();
255254
}
256-
return processRelations(entityMetaData, entityToBeSaved, isEntityNew, inDatabase, instance);
255+
processRelations(entityMetaData, instance, propertyAccessor, inDatabase, isEntityNew);
256+
257+
return propertyAccessor.getBean();
257258
}
258259

259260
private <T> DynamicLabels determineDynamicLabels(T entityToBeSaved, Neo4jPersistentEntity<?> entityMetaData,
@@ -305,37 +306,42 @@ public <T> List<T> saveAll(Iterable<T> instances) {
305306
return entities.stream().map(e -> saveImpl(e, databaseName)).collect(Collectors.toList());
306307
}
307308

308-
// we need to determine the `isNew` state of the entities before calling the id generator
309-
List<Boolean> isNewIndicator = entities.stream().map(entity ->
310-
neo4jMappingContext.getPersistentEntity(entity.getClass()).isNew(entity)
311-
).collect(Collectors.toList());
309+
class Tuple3<T> {
310+
T t1;
311+
boolean t2;
312+
T t3;
313+
314+
Tuple3(T t1, boolean t2, T t3) {
315+
this.t1 = t1;
316+
this.t2 = t2;
317+
this.t3 = t3;
318+
}
319+
}
312320

313-
List<T> entitiesToBeSaved = entities.stream().map(eventSupport::maybeCallBeforeBind)
321+
List<Tuple3<T>> entitiesToBeSaved = entities.stream()
322+
.map(e -> new Tuple3<>(e, neo4jMappingContext.getPersistentEntity(e.getClass()).isNew(e), eventSupport.maybeCallBeforeBind(e)))
314323
.collect(Collectors.toList());
315324

316325
// Save roots
317326
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass);
318-
List<Map<String, Object>> entityList = entitiesToBeSaved.stream().map(binderFunction)
327+
List<Map<String, Object>> entityList = entitiesToBeSaved.stream().map(h -> h.t3).map(binderFunction)
319328
.collect(Collectors.toList());
320329
ResultSummary resultSummary = neo4jClient
321330
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
322331
.in(databaseName)
323332
.bind(entityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM).run();
324333

325-
// Save related
326-
entitiesToBeSaved.forEach(entityToBeSaved -> {
327-
int positionInList = entitiesToBeSaved.indexOf(entityToBeSaved);
328-
processRelations(entityMetaData, entityToBeSaved, isNewIndicator.get(positionInList), databaseName,
329-
entities.get(positionInList));
330-
});
331-
332334
SummaryCounters counters = resultSummary.counters();
333335
log.debug(() -> String.format(
334336
"Created %d and deleted %d nodes, created %d and deleted %d relationships and set %d properties.",
335337
counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(),
336338
counters.relationshipsDeleted(), counters.propertiesSet()));
337339

338-
return entitiesToBeSaved;
340+
// Save related
341+
return entitiesToBeSaved.stream().map(t -> {
342+
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(t.t3);
343+
return processRelations(entityMetaData, t.t1, propertyAccessor, databaseName, t.t2);
344+
}).collect(Collectors.toList());
339345
}
340346

341347
@Override
@@ -436,27 +442,37 @@ private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, String
436442
return toExecutableQuery(preparedQuery);
437443
}
438444

439-
private <T> T processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject,
440-
boolean isParentObjectNew, @Nullable String inDatabase, Object parentEntity) {
445+
/**
446+
* Starts of processing of the relationships.
447+
*
448+
* @param neo4jPersistentEntity The description of the instance to save
449+
* @param originalInstance The original parent instance. It is paramount to pass in the original instance (prior
450+
* to generating the id and prior to eventually create new instances via the property accessor,
451+
* so that we can reliable stop traversing relationships.
452+
* @param parentPropertyAccessor The property accessor of the parent, to modify the relationships
453+
* @param isParentObjectNew A flag if the parent was new
454+
*/
455+
private <T> T processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, T originalInstance,
456+
PersistentPropertyAccessor<?> parentPropertyAccessor,
457+
@Nullable String inDatabase, boolean isParentObjectNew) {
441458

442-
return processNestedRelations(neo4jPersistentEntity, parentObject, isParentObjectNew, inDatabase,
443-
new NestedRelationshipProcessingStateMachine(parentEntity));
459+
return processNestedRelations(neo4jPersistentEntity, parentPropertyAccessor, isParentObjectNew, inDatabase,
460+
new NestedRelationshipProcessingStateMachine(originalInstance));
444461
}
445462

446-
private <T> T processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, Object parentObject,
463+
private <T> T processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, PersistentPropertyAccessor<?> propertyAccessor,
447464
boolean isParentObjectNew, @Nullable String inDatabase, NestedRelationshipProcessingStateMachine stateMachine) {
448465

449-
PersistentPropertyAccessor<?> propertyAccessor = sourceEntity.getPropertyAccessor(parentObject);
450466
Object fromId = propertyAccessor.getProperty(sourceEntity.getRequiredIdProperty());
451467

452468
sourceEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
453469

454470
// create context to bundle parameters
455-
NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor,
456-
sourceEntity);
471+
NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor, sourceEntity);
457472

473+
Object rawValue = relationshipContext.getValue();
458474
Collection<?> relatedValuesToStore = MappingSupport.unifyRelationshipValue(relationshipContext.getInverse(),
459-
relationshipContext.getValue());
475+
rawValue);
460476

461477
RelationshipDescription relationshipDescription = relationshipContext.getRelationship();
462478
RelationshipDescription relationshipDescriptionObverse = relationshipDescription.getRelationshipObverse();
@@ -511,22 +527,27 @@ private <T> T processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, Obje
511527

512528
stateMachine.markRelationshipAsProcessed(fromId, relationshipDescription);
513529

530+
Neo4jPersistentProperty relationshipProperty = association.getInverse();
531+
532+
RelationshipHandler relationshipHandler = RelationshipHandler.forProperty(relationshipProperty, rawValue);
533+
514534
for (Object relatedValueToStore : relatedValuesToStore) {
515535

516536
// here a map entry is not always anymore a dynamic association
517-
Object relatedNode = relationshipContext.identifyAndExtractRelationshipTargetNode(relatedValueToStore);
518-
Neo4jPersistentEntity<?> targetEntity = neo4jMappingContext.getPersistentEntity(relatedNode.getClass());
537+
Object relatedObjectBeforeCallbacks = relationshipContext.identifyAndExtractRelationshipTargetNode(relatedValueToStore);
538+
Neo4jPersistentEntity<?> targetEntity = neo4jMappingContext.getPersistentEntity(relatedObjectBeforeCallbacks.getClass());
519539

520-
boolean isEntityNew = targetEntity.isNew(relatedNode);
540+
boolean isEntityNew = targetEntity.isNew(relatedObjectBeforeCallbacks);
521541

522-
relatedNode = eventSupport.maybeCallBeforeBind(relatedNode);
542+
Object newRelatedObject = eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacks);
523543

524544
Long relatedInternalId;
525545
// No need to save values if processed
526546
if (stateMachine.hasProcessedValue(relatedValueToStore)) {
527-
relatedInternalId = queryRelatedNode(relatedNode, targetEntity, inDatabase);
547+
Object newRelatedObjectForQuery = stateMachine.getProcessedAs(newRelatedObject);
548+
relatedInternalId = queryRelatedNode(newRelatedObjectForQuery, targetEntity, inDatabase);
528549
} else {
529-
relatedInternalId = saveRelatedNode(relatedNode, targetEntity, inDatabase);
550+
relatedInternalId = saveRelatedNode(newRelatedObject, targetEntity, inDatabase);
530551
}
531552
stateMachine.markValueAsProcessed(relatedValueToStore);
532553

@@ -556,16 +577,27 @@ private <T> T processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, Obje
556577
.setProperty(idProperty, relationshipInternalId.get());
557578
}
558579

559-
PersistentPropertyAccessor<?> targetPropertyAccessor = targetEntity.getPropertyAccessor(relatedNode);
560-
// if an internal id is used this must get set to link this entity in the next iteration
580+
PersistentPropertyAccessor<?> targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject);
581+
// if an internal id is used this must be set to link this entity in the next iteration
561582
if (targetEntity.isUsingInternalIds()) {
562583
targetPropertyAccessor.setProperty(targetEntity.getRequiredIdProperty(), relatedInternalId);
584+
stateMachine.markValueAsProcessedAs(newRelatedObject, targetPropertyAccessor.getBean());
563585
}
586+
564587
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
565-
processNestedRelations(targetEntity, targetPropertyAccessor.getBean(), isEntityNew, inDatabase, stateMachine);
588+
processNestedRelations(targetEntity, targetPropertyAccessor, isEntityNew, inDatabase, stateMachine);
566589
}
590+
591+
Object potentiallyRecreatedNewRelatedObject = MappingSupport.getRelationshipOrRelationshipPropertiesObject(neo4jMappingContext,
592+
relationshipDescription.hasRelationshipProperties(),
593+
relationshipProperty.isDynamicAssociation(),
594+
relatedValueToStore,
595+
targetPropertyAccessor);
596+
597+
relationshipHandler.handle(relatedValueToStore, relatedObjectBeforeCallbacks, potentiallyRecreatedNewRelatedObject);
567598
}
568599

600+
relationshipHandler.applyFinalResultToOwner(propertyAccessor);
569601
});
570602

571603
return (T) propertyAccessor.getBean();
@@ -720,9 +752,11 @@ private GenericQueryAndParameters createQueryAndParameters(Neo4jPersistentEntity
720752
.prepareMatchOf(entityMetaData, queryFragments.getMatchOn(), queryFragments.getCondition())
721753
.returning(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).build();
722754

755+
Map<String, Object> usedParameters = new HashMap<>(parameters);
756+
usedParameters.putAll(rootNodesStatement.getParameters());
723757
final Collection<Long> rootNodeIds = new HashSet<>((Collection<Long>) neo4jClient
724758
.query(renderer.render(rootNodesStatement)).in(getDatabaseName())
725-
.bindAll(parameters)
759+
.bindAll(usedParameters)
726760
.fetch()
727761
.one()
728762
.map(values -> values.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE))
@@ -742,6 +776,8 @@ private GenericQueryAndParameters createQueryAndParameters(Neo4jPersistentEntity
742776
.prepareMatchOf(entityMetaData, relationshipDescription, queryFragments.getMatchOn(), queryFragments.getCondition())
743777
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
744778

779+
usedParameters = new HashMap<>(parameters);
780+
usedParameters.putAll(statement.getParameters());
745781
neo4jClient.query(renderer.render(statement)).in(getDatabaseName())
746782
.bindAll(parameters)
747783
.fetch()

0 commit comments

Comments
 (0)