@@ -232,13 +232,25 @@ private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, Obj
232232 @ Override
233233 public <T > Mono <T > save (T instance ) {
234234
235- return getDatabaseName ().flatMap (databaseName -> saveImpl (instance , databaseName .getValue ()));
235+ return getDatabaseName ().flatMap (databaseName -> saveImpl (instance , databaseName .getValue (), null ));
236236 }
237237
238- private <T > Mono <T > saveImpl (T instance , @ Nullable String inDatabase ) {
238+ private <T > Mono <T > saveImpl (T instance , @ Nullable String inDatabase , @ Nullable NestedRelationshipProcessingStateMachine stateMachine ) {
239+
240+ if (stateMachine != null && stateMachine .hasProcessedValue (instance )) {
241+ return Mono .just (instance );
242+ }
239243
240244 Neo4jPersistentEntity entityMetaData = neo4jMappingContext .getPersistentEntity (instance .getClass ());
241245 boolean isNewEntity = entityMetaData .isNew (instance );
246+
247+ NestedRelationshipProcessingStateMachine finalStateMachine ;
248+ if (stateMachine == null ) {
249+ finalStateMachine = new NestedRelationshipProcessingStateMachine (neo4jMappingContext );
250+ } else {
251+ finalStateMachine = stateMachine ;
252+ }
253+
242254 return Mono .just (instance ).flatMap (eventSupport ::maybeCallBeforeBind )
243255 .flatMap (entityToBeSaved -> determineDynamicLabels (entityToBeSaved , entityMetaData , inDatabase )).flatMap (t -> {
244256 T entityToBeSaved = t .getT1 ();
@@ -258,12 +270,13 @@ private <T> Mono<T> saveImpl(T instance, @Nullable String inDatabase) {
258270
259271 PersistentPropertyAccessor <T > propertyAccessor = entityMetaData .getPropertyAccessor (entityToBeSaved );
260272 return idMono .doOnNext (newOrUpdatedNode -> {
261- if (entityMetaData .isUsingInternalIds ()) {
262- propertyAccessor .setProperty (entityMetaData .getRequiredIdProperty (), newOrUpdatedNode .id ());
263- }
264- TemplateSupport .updateVersionPropertyIfPossible (entityMetaData , propertyAccessor , newOrUpdatedNode );
265- }).map (Entity ::id )
266- .flatMap (internalId -> processRelations (entityMetaData , instance , internalId , propertyAccessor , inDatabase , isNewEntity ));
273+ if (entityMetaData .isUsingInternalIds ()) {
274+ propertyAccessor .setProperty (entityMetaData .getRequiredIdProperty (), newOrUpdatedNode .id ());
275+ }
276+ TemplateSupport .updateVersionPropertyIfPossible (entityMetaData , propertyAccessor , newOrUpdatedNode );
277+ finalStateMachine .markValueAsProcessed (instance , newOrUpdatedNode .id ());
278+ }).map (Entity ::id )
279+ .flatMap (internalId -> processRelations (entityMetaData , propertyAccessor , isNewEntity , finalStateMachine , inDatabase ));
267280 });
268281 }
269282
@@ -312,8 +325,9 @@ public <T> Flux<T> saveAll(Iterable<T> instances) {
312325 || entityMetaData .getDynamicLabelsProperty ().isPresent ()) {
313326 log .debug ("Saving entities using single statements." );
314327
328+ NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine (neo4jMappingContext );
315329 return getDatabaseName ().flatMapMany (
316- databaseName -> Flux .fromIterable (entities ).flatMap (e -> this .saveImpl (e , databaseName .getValue ())));
330+ databaseName -> Flux .fromIterable (entities ).concatMap (e -> this .saveImpl (e , databaseName .getValue (), stateMachine )));
317331 }
318332
319333 Function <T , Map <String , Object >> binderFunction = neo4jMappingContext .getRequiredBinderFunctionFor (domainClass );
@@ -343,8 +357,7 @@ public <T> Flux<T> saveAll(Iterable<T> instances) {
343357 Neo4jPersistentProperty idProperty = entityMetaData .getRequiredIdProperty ();
344358 Object id = convertIdValues (idProperty , propertyAccessor .getProperty (idProperty ));
345359 Long internalId = idToInternalIdMapping .get (id );
346- return processRelations (entityMetaData , t .getT1 (), internalId ,
347- propertyAccessor , databaseName .getValue (), t .getT2 ());
360+ return processRelations (entityMetaData , propertyAccessor , t .getT2 (), new NestedRelationshipProcessingStateMachine (neo4jMappingContext , t .getT1 (), internalId ), databaseName .getValue ());
348361 }))
349362 ));
350363 }
@@ -445,8 +458,8 @@ private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType,
445458 private <T > Mono <ExecutableQuery <T >> createExecutableQuery (Class <T > domainType ,
446459 QueryFragmentsAndParameters queryFragmentsAndParameters ) {
447460
448- Neo4jPersistentEntity <?> entityMetaData = neo4jMappingContext .getPersistentEntity (domainType );
449461 QueryFragmentsAndParameters .QueryFragments queryFragments = queryFragmentsAndParameters .getQueryFragments ();
462+ Neo4jPersistentEntity <?> entityMetaData = (Neo4jPersistentEntity <?>) queryFragmentsAndParameters .getNodeDescription ();
450463
451464 boolean containsPossibleCircles = entityMetaData != null && entityMetaData .containsPossibleCircles (queryFragments ::includeField );
452465 if (containsPossibleCircles && !queryFragments .isScalarValueReturn ()) {
@@ -574,24 +587,24 @@ Publisher<Tuple2<Collection<Long>, Collection<Long>>>> iterateAndMapNextLevel(
574587 * Starts of processing of the relationships.
575588 *
576589 * @param neo4jPersistentEntity The description of the instance to save
577- * @param originalInstance The original parent instance. It is paramount to pass in the original instance (prior
578- * to generating the id and prior to eventually create new instances via the property accessor,
579- * so that we can reliable stop traversing relationships.
580590 * @param parentPropertyAccessor The property accessor of the parent, to modify the relationships
581591 * @param isParentObjectNew A flag if the parent was new
592+ * @param stateMachine Statemachine containing process data
593+ * @param inDatabase Optional target database
582594 * @param <T> The type of the entity to save
583595 * @return A mono representing the whole stream of save operations.
584596 */
585- private <T > Mono <T > processRelations (Neo4jPersistentEntity <?> neo4jPersistentEntity , T originalInstance ,
586- Long internalId , PersistentPropertyAccessor <?> parentPropertyAccessor ,
587- @ Nullable String inDatabase , boolean isParentObjectNew ) {
597+ private <T > Mono <T > processRelations (Neo4jPersistentEntity <?> neo4jPersistentEntity ,
598+ PersistentPropertyAccessor <?> parentPropertyAccessor ,
599+ boolean isParentObjectNew , NestedRelationshipProcessingStateMachine stateMachine ,
600+ @ Nullable String inDatabase ) {
588601
589- return processNestedRelations (neo4jPersistentEntity , parentPropertyAccessor , isParentObjectNew , inDatabase ,
590- new NestedRelationshipProcessingStateMachine ( originalInstance , internalId ) );
602+ return processNestedRelations (neo4jPersistentEntity , parentPropertyAccessor , isParentObjectNew , stateMachine ,
603+ inDatabase );
591604 }
592605
593606 private <T > Mono <T > processNestedRelations (Neo4jPersistentEntity <?> sourceEntity , PersistentPropertyAccessor <?> parentPropertyAccessor ,
594- boolean isParentObjectNew , @ Nullable String inDatabase , NestedRelationshipProcessingStateMachine stateMachine ) {
607+ boolean isParentObjectNew , NestedRelationshipProcessingStateMachine stateMachine , @ Nullable String inDatabase ) {
595608
596609 Object fromId = parentPropertyAccessor .getProperty (sourceEntity .getRequiredIdProperty ());
597610 List <Mono <Void >> relationshipDeleteMonos = new ArrayList <>();
@@ -665,36 +678,53 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
665678 Flux <RelationshipHandler > relationshipCreation = Flux .fromIterable (relatedValuesToStore ).concatMap (relatedValueToStore -> {
666679
667680 Object relatedObjectBeforeCallbacksApplied = relationshipContext .identifyAndExtractRelationshipTargetNode (relatedValueToStore );
668- return Mono .deferContextual (ctx -> eventSupport
669- .maybeCallBeforeBind (relatedObjectBeforeCallbacksApplied )
681+ return Mono .deferContextual (ctx ->
682+
683+ (stateMachine .hasProcessedValue (relatedObjectBeforeCallbacksApplied )
684+ ? Mono .just (stateMachine .getProcessedAs (relatedObjectBeforeCallbacksApplied ))
685+ : eventSupport .maybeCallBeforeBind (relatedObjectBeforeCallbacksApplied ))
686+
670687 .flatMap (newRelatedObject -> {
671688 Neo4jPersistentEntity <?> targetEntity = neo4jMappingContext .getPersistentEntity (relatedObjectBeforeCallbacksApplied .getClass ());
672689
673- Mono <Tuple2 <Long , Long >> queryOrSave ;
690+ Mono <Tuple2 <Long [] , Long [] >> queryOrSave ;
674691 long noVersion = Long .MIN_VALUE ;
692+ long noId = Long .MIN_VALUE ;
675693 if (stateMachine .hasProcessedValue (relatedValueToStore )) {
676- queryOrSave = Mono .just (stateMachine .getInternalId (relatedObjectBeforeCallbacksApplied ))
677- .map (id -> Tuples .of (id , noVersion ));
694+ queryOrSave = Mono .just (new Long [] { stateMachine .getInternalId (relatedObjectBeforeCallbacksApplied ) } )
695+ .map (id -> Tuples .of (id , new Long [ 1 ] ));
678696 } else {
679697 queryOrSave = saveRelatedNode (newRelatedObject , targetEntity , inDatabase )
680- .map (entity -> Tuples .of (entity .id (), targetEntity .hasVersionProperty () ?
681- entity .get (targetEntity .getVersionProperty ().getPropertyName ())
682- .asLong () :
683- noVersion ));
698+ .doOnNext (entity -> {
699+ stateMachine .markValueAsProcessed (relatedValueToStore , entity .id ());
700+ })
701+ .map (entity -> {
702+ Long version = targetEntity .hasVersionProperty () ?
703+ entity .get (targetEntity .getVersionProperty ().getPropertyName ()).asLong () :
704+ null ;
705+ return Tuples .of (
706+ new Long [] { entity .id () },
707+ new Long [] { version });
708+ });
684709 }
685710
686711 return queryOrSave .flatMap (idAndVersion -> {
687- long relatedInternalId = idAndVersion .getT1 ();
688- stateMachine .markValueAsProcessed (relatedValueToStore , relatedInternalId );
712+ Long relatedInternalId = idAndVersion .getT1 ()[0 ];
689713 // if an internal id is used this must be set to link this entity in the next iteration
690714 PersistentPropertyAccessor <?> targetPropertyAccessor = targetEntity .getPropertyAccessor (newRelatedObject );
691715 if (targetEntity .isUsingInternalIds ()) {
692- targetPropertyAccessor .setProperty (targetEntity .getRequiredIdProperty (), relatedInternalId );
693- stateMachine .markValueAsProcessedAs (newRelatedObject , targetPropertyAccessor .getBean ());
716+ Neo4jPersistentProperty requiredIdProperty = targetEntity .getRequiredIdProperty ();
717+ if (relatedInternalId == null
718+ && targetPropertyAccessor .getProperty (requiredIdProperty ) != null ) {
719+ relatedInternalId = (Long ) targetPropertyAccessor .getProperty (requiredIdProperty );
720+ } else if (targetPropertyAccessor .getProperty (requiredIdProperty ) == null ) {
721+ targetPropertyAccessor .setProperty (requiredIdProperty , relatedInternalId );
722+ }
694723 }
695- if (targetEntity .hasVersionProperty () && idAndVersion .getT2 () != noVersion ) {
696- targetPropertyAccessor .setProperty (targetEntity .getVersionProperty (), idAndVersion .getT2 ());
724+ if (targetEntity .hasVersionProperty () && idAndVersion .getT2 ()[ 0 ] != null ) {
725+ targetPropertyAccessor .setProperty (targetEntity .getVersionProperty (), idAndVersion .getT2 ()[ 0 ] );
697726 }
727+ stateMachine .markValueAsProcessedAs (relatedObjectBeforeCallbacksApplied , targetPropertyAccessor .getBean ());
698728 stateMachine .markRelationshipAsProcessed (relatedInternalId , relationshipDescription .getRelationshipObverse ());
699729
700730 Object idValue = idProperty != null
@@ -726,7 +756,7 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
726756
727757 Mono <Object > nestedRelationshipsSignal = null ;
728758 if (processState != ProcessState .PROCESSED_ALL_VALUES ) {
729- nestedRelationshipsSignal = processNestedRelations (targetEntity , targetPropertyAccessor , targetEntity .isNew (newRelatedObject ), inDatabase , stateMachine );
759+ nestedRelationshipsSignal = processNestedRelations (targetEntity , targetPropertyAccessor , targetEntity .isNew (newRelatedObject ), stateMachine , inDatabase );
730760 }
731761
732762 Mono <Object > getRelationshipOrRelationshipPropertiesObject = Mono .fromSupplier (() -> MappingSupport .getRelationshipOrRelationshipPropertiesObject (
0 commit comments