@@ -418,19 +418,25 @@ <T, R> Flux<R> doSave(Iterable<R> instances, Class<T> domainType) {
418418 getProjectionFactory (), neo4jMappingContext );
419419
420420 NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine (neo4jMappingContext );
421+ Collection <Object > knownRelationshipsIds = new HashSet <>();
421422 EntityFromDtoInstantiatingConverter <T > converter = new EntityFromDtoInstantiatingConverter <>(domainType , neo4jMappingContext );
422423 return Flux .fromIterable (instances )
423424 .concatMap (instance -> {
424425 T domainObject = converter .convert (instance );
425426
426427 @ SuppressWarnings ("unchecked" )
427- Mono <R > result = transactionalOperator .transactional (saveImpl (domainObject , pps , stateMachine )
428+ Mono <R > result = transactionalOperator .transactional (saveImpl (domainObject , pps , stateMachine , knownRelationshipsIds )
428429 .map (savedEntity -> (R ) new DtoInstantiatingConverter (resultType , neo4jMappingContext ).convertDirectly (savedEntity )));
429430 return result ;
430431 });
431432 }
432433
434+
433435 private <T > Mono <T > saveImpl (T instance , @ Nullable Collection <PropertyFilter .ProjectedPath > includedProperties , @ Nullable NestedRelationshipProcessingStateMachine stateMachine ) {
436+ return saveImpl (instance , includedProperties , stateMachine , new HashSet <>());
437+ }
438+
439+ private <T > Mono <T > saveImpl (T instance , @ Nullable Collection <PropertyFilter .ProjectedPath > includedProperties , @ Nullable NestedRelationshipProcessingStateMachine stateMachine , Collection <Object > knownRelationshipsIds ) {
434440
435441 if (stateMachine != null && stateMachine .hasProcessedValue (instance )) {
436442 return Mono .just (instance );
@@ -479,7 +485,7 @@ private <T> Mono<T> saveImpl(T instance, @Nullable Collection<PropertyFilter.Pro
479485 TemplateSupport .updateVersionPropertyIfPossible (entityMetaData , propertyAccessor , newOrUpdatedNode );
480486 finalStateMachine .markEntityAsProcessed (instance , elementId );
481487 }).map (IdentitySupport ::getElementId )
482- .flatMap (internalId -> processRelations (entityMetaData , propertyAccessor , isNewEntity , finalStateMachine , binderFunction .filter ));
488+ .flatMap (internalId -> processRelations (entityMetaData , propertyAccessor , isNewEntity , finalStateMachine , knownRelationshipsIds , binderFunction .filter ));
483489 });
484490 }
485491
@@ -594,7 +600,7 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Collection<Prop
594600 Function <T , Map <String , Object >> binderFunction = TemplateSupport .createAndApplyPropertyFilter (
595601 pps , entityMetaData ,
596602 neo4jMappingContext .getRequiredBinderFunctionFor ((Class <T >) domainClass ));
597- return Flux .fromIterable (entities )
603+ return ( Flux < T >) Flux . deferContextual (( ctx ) -> Flux .fromIterable (entities )
598604 // Map all entities into a tuple <Original, OriginalWasNew>
599605 .map (e -> Tuples .of (e , entityMetaData .isNew (e )))
600606 // Map that tuple into a tuple <<Original, OriginalWasNew>, PotentiallyModified>
@@ -618,11 +624,16 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Collection<Prop
618624 .concatMap (t -> {
619625 PersistentPropertyAccessor <T > propertyAccessor = entityMetaData .getPropertyAccessor (t .getT3 ());
620626 Neo4jPersistentProperty idProperty = entityMetaData .getRequiredIdProperty ();
621- Object id = convertIdValues ( idProperty , propertyAccessor . getProperty ( idProperty ));
622- String internalId = idToInternalIdMapping .get (id );
623- return processRelations ( entityMetaData , propertyAccessor , t . getT2 (), new NestedRelationshipProcessingStateMachine ( neo4jMappingContext , t . getT1 (), internalId ),
627+ return processRelations ( entityMetaData , propertyAccessor , t . getT2 (),
628+ ctx .get ("stateMachine" ),
629+ ctx . get ( "knownRelIds" ),
624630 TemplateSupport .computeIncludePropertyPredicate (pps , entityMetaData ));
625631 }))
632+ ))
633+ .contextWrite (ctx ->
634+ ctx
635+ .put ("stateMachine" , new NestedRelationshipProcessingStateMachine (neo4jMappingContext , null , null ))
636+ .put ("knownRelIds" , new HashSet <>())
626637 );
627638 }
628639
@@ -878,16 +889,19 @@ private <T> Mono<T> processRelations(
878889 PersistentPropertyAccessor <?> parentPropertyAccessor ,
879890 boolean isParentObjectNew ,
880891 NestedRelationshipProcessingStateMachine stateMachine ,
892+ Collection <Object > knownRelationshipsIds ,
881893 PropertyFilter includeProperty
882894 ) {
883895
884896 PropertyFilter .RelaxedPropertyPath startingPropertyPath = PropertyFilter .RelaxedPropertyPath .withRootType (neo4jPersistentEntity .getUnderlyingClass ());
885897 return processNestedRelations (neo4jPersistentEntity , parentPropertyAccessor , isParentObjectNew ,
886- stateMachine , includeProperty , startingPropertyPath );
898+ stateMachine , knownRelationshipsIds , includeProperty , startingPropertyPath );
887899 }
888900
889901 private <T > Mono <T > processNestedRelations (Neo4jPersistentEntity <?> sourceEntity , PersistentPropertyAccessor <?> parentPropertyAccessor ,
890- boolean isParentObjectNew , NestedRelationshipProcessingStateMachine stateMachine , PropertyFilter includeProperty , PropertyFilter .RelaxedPropertyPath previousPath ) {
902+ boolean isParentObjectNew , NestedRelationshipProcessingStateMachine stateMachine ,
903+ Collection <Object > knownRelationshipsIds ,
904+ PropertyFilter includeProperty , PropertyFilter .RelaxedPropertyPath previousPath ) {
891905
892906 Object fromId = parentPropertyAccessor .getProperty (sourceEntity .getRequiredIdProperty ());
893907 List <Mono <Void >> relationshipDeleteMonos = new ArrayList <>();
@@ -932,7 +946,6 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
932946 boolean canUseElementId = TemplateSupport .rendererRendersElementId (renderer );
933947 if (!isParentObjectNew && !stateMachine .hasProcessedRelationship (fromId , relationshipDescription )) {
934948
935- List <Object > knownRelationshipsIds = new ArrayList <>();
936949 if (idProperty != null ) {
937950 for (Object relatedValueToStore : relatedValuesToStore ) {
938951 if (relatedValueToStore == null ) {
@@ -1021,7 +1034,7 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
10211034 TemplateSupport .updateVersionPropertyIfPossible (targetEntity , targetPropertyAccessor , savedEntity );
10221035 }
10231036 stateMachine .markAsAliased (relatedObjectBeforeCallbacksApplied , targetPropertyAccessor .getBean ());
1024- stateMachine .markRelationshipAsProcessed (possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId ,
1037+ stateMachine .markRelationshipAsProcessed (possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId ,
10251038 relationshipDescription .getRelationshipObverse ());
10261039
10271040 Object idValue = idProperty != null
@@ -1037,43 +1050,63 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
10371050 properties .put (Constants .FROM_ID_PARAMETER_NAME , convertIdValues (sourceEntity .getRequiredIdProperty (), fromId ));
10381051 properties .put (Constants .TO_ID_PARAMETER_NAME , relatedInternalId );
10391052 properties .put (Constants .NAME_OF_KNOWN_RELATIONSHIP_PARAM , idValue );
1053+ var update = true ;
1054+ if (!relationshipDescription .isDynamic () && relationshipDescription .hasRelationshipProperties ()) {
1055+ var hlp = ((MappingSupport .RelationshipPropertiesWithEntityHolder ) relatedValueToStore );
1056+ var hasProcessedRelationshipEntity = stateMachine .hasProcessedRelationshipEntity (parentPropertyAccessor .getBean (), hlp .getRelatedEntity (), relationshipContext .getRelationship ());
1057+ if (hasProcessedRelationshipEntity ) {
1058+ stateMachine .requireIdUpdate (sourceEntity , relationshipDescription , canUseElementId , fromId , relatedInternalId , relationshipContext , relatedValueToStore , idProperty );
1059+ update = false ;
1060+ } else {
1061+ stateMachine .storeProcessRelationshipEntity (hlp , parentPropertyAccessor .getBean (), hlp .getRelatedEntity (), relationshipContext .getRelationship ());
1062+ }
1063+ }
10401064 List <Object > rows = new ArrayList <>();
10411065 rows .add (properties );
10421066 statementHolder = statementHolder .addProperty (Constants .NAME_OF_RELATIONSHIP_LIST_PARAM , rows );
10431067 // in case of no properties the bind will just return an empty map
1044- return neo4jClient
1045- .query (renderer .render (statementHolder .getStatement ()))
1046- .bind (convertIdValues (sourceEntity .getRequiredIdProperty (), fromId )) //
1068+ if (update ) {
1069+ return neo4jClient
1070+ .query (renderer .render (statementHolder .getStatement ()))
1071+ .bind (convertIdValues (sourceEntity .getRequiredIdProperty (), fromId )) //
10471072 .to (Constants .FROM_ID_PARAMETER_NAME ) //
1048- .bind (relatedInternalId ) //
1073+ .bind (relatedInternalId ) //
10491074 .to (Constants .TO_ID_PARAMETER_NAME ) //
1050- .bind (idValue ) //
1051- .to (Constants .NAME_OF_KNOWN_RELATIONSHIP_PARAM ) //
1052- .bindAll (statementHolder .getProperties ())
1053- .fetchAs (Object .class )
1054- .mappedBy ((t , r ) -> IdentitySupport .mapperForRelatedIdValues (idProperty ).apply (r ))
1055- .one ()
1056- .flatMap (relationshipInternalId -> {
1057- if (idProperty != null && isNewRelationship ) {
1058- relationshipContext
1059- .getRelationshipPropertiesPropertyAccessor (relatedValueToStore )
1060- .setProperty (idProperty , relationshipInternalId );
1061- }
1062-
1063- Mono <Object > nestedRelationshipsSignal = null ;
1064- if (processState != ProcessState .PROCESSED_ALL_VALUES ) {
1065- nestedRelationshipsSignal = processNestedRelations (targetEntity , targetPropertyAccessor , targetEntity .isNew (newRelatedObject ), stateMachine , includeProperty , currentPropertyPath );
1066- }
1067-
1068- Mono <Object > getRelationshipOrRelationshipPropertiesObject = Mono .fromSupplier (() -> MappingSupport .getRelationshipOrRelationshipPropertiesObject (
1069- neo4jMappingContext ,
1070- relationshipDescription .hasRelationshipProperties (),
1071- relationshipProperty .isDynamicAssociation (),
1072- relatedValueToStore ,
1073- targetPropertyAccessor ));
1074- return nestedRelationshipsSignal == null ? getRelationshipOrRelationshipPropertiesObject :
1075- nestedRelationshipsSignal .then (getRelationshipOrRelationshipPropertiesObject );
1076- });
1075+ .bind (idValue ) //
1076+ .to (Constants .NAME_OF_KNOWN_RELATIONSHIP_PARAM ) //
1077+ .bindAll (statementHolder .getProperties ())
1078+ .fetchAs (Object .class )
1079+ .mappedBy ((t , r ) -> IdentitySupport .mapperForRelatedIdValues (idProperty ).apply (r ))
1080+ .one ()
1081+ .flatMap (relationshipInternalId -> {
1082+ if (idProperty != null && isNewRelationship ) {
1083+ relationshipContext
1084+ .getRelationshipPropertiesPropertyAccessor (relatedValueToStore )
1085+ .setProperty (idProperty , relationshipInternalId );
1086+ knownRelationshipsIds .add (relationshipInternalId );
1087+ }
1088+
1089+ Mono <Object > nestedRelationshipsSignal = null ;
1090+ if (processState != ProcessState .PROCESSED_ALL_VALUES ) {
1091+ nestedRelationshipsSignal = processNestedRelations (targetEntity , targetPropertyAccessor , targetEntity .isNew (newRelatedObject ), stateMachine , knownRelationshipsIds , includeProperty , currentPropertyPath );
1092+ }
1093+
1094+ Mono <Object > getRelationshipOrRelationshipPropertiesObject = Mono .fromSupplier (() -> MappingSupport .getRelationshipOrRelationshipPropertiesObject (
1095+ neo4jMappingContext ,
1096+ relationshipDescription .hasRelationshipProperties (),
1097+ relationshipProperty .isDynamicAssociation (),
1098+ relatedValueToStore ,
1099+ targetPropertyAccessor ));
1100+ return nestedRelationshipsSignal == null ? getRelationshipOrRelationshipPropertiesObject :
1101+ nestedRelationshipsSignal .then (getRelationshipOrRelationshipPropertiesObject );
1102+ });
1103+ }
1104+ return Mono .fromSupplier (() -> MappingSupport .getRelationshipOrRelationshipPropertiesObject (
1105+ neo4jMappingContext ,
1106+ relationshipDescription .hasRelationshipProperties (),
1107+ relationshipProperty .isDynamicAssociation (),
1108+ relatedValueToStore ,
1109+ targetPropertyAccessor ));
10771110 })
10781111 .doOnNext (potentiallyRecreatedRelatedObject -> {
10791112 RelationshipHandler handler = ctx .get (CONTEXT_RELATIONSHIP_HANDLER );
@@ -1095,11 +1128,24 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
10951128 .thenMany (Flux .concat (relationshipCreationCreations ))
10961129 .doOnNext (objects -> objects .applyFinalResultToOwner (parentPropertyAccessor ))
10971130 .checkpoint ()
1131+ .then (stateMachine .updateRelationshipIds (this ::getRelationshipId ))
10981132 .then (Mono .fromSupplier (parentPropertyAccessor ::getBean ));
10991133 return deleteAndThanCreateANew ;
11001134
11011135 }
11021136
1137+ private Mono <Object > getRelationshipId (Statement statement , Neo4jPersistentProperty idProperty , Object fromId , Object toId ) {
1138+
1139+ return neo4jClient .query (renderer .render (statement ))
1140+ .bind (convertIdValues (idProperty , fromId )) //
1141+ .to (Constants .FROM_ID_PARAMETER_NAME ) //
1142+ .bind (toId ) //
1143+ .to (Constants .TO_ID_PARAMETER_NAME ) //
1144+ .fetchAs (Object .class )
1145+ .mappedBy ((t , r ) -> IdentitySupport .mapperForRelatedIdValues (idProperty ).apply (r ))
1146+ .one ();
1147+ }
1148+
11031149 // The pendant to {@link #saveRelatedNode(Object, Neo4jPersistentEntity, PropertyFilter, PropertyFilter.RelaxedPropertyPath)}
11041150 // We can't do without a query, as we need to refresh the internal id
11051151 private Mono <Entity > loadRelatedNode (NodeDescription <?> targetNodeDescription , Object relatedInternalId ) {
0 commit comments