diff --git a/pom.xml b/pom.xml
index cad1281350..03956087cb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
org.springframework.data
spring-data-neo4j
- 6.0.7-SNAPSHOT
+ 6.0.7-GH-2196-SNAPSHOT
Spring Data Neo4j
Next generation Object-Graph-Mapping for Spring Data.
diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java
index 24c5dc4fe8..eac3bdafed 100644
--- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java
+++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java
@@ -531,18 +531,27 @@ private T processNestedRelations(Neo4jPersistentEntity> sourceEntity, Obje
}
stateMachine.markValueAsProcessed(relatedValueToStore);
+ Object idValue = idProperty != null
+ ? relationshipContext
+ .getRelationshipPropertiesPropertyAccessor(relatedValueToStore).getProperty(idProperty)
+ : null;
+
+ boolean isNewRelationship = idValue == null;
+
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatement(
- sourceEntity, relationshipContext, relatedValueToStore);
+ sourceEntity, relationshipContext, relatedValueToStore, isNewRelationship);
Optional relationshipInternalId = neo4jClient.query(renderer.render(statementHolder.getStatement())).in(inDatabase)
.bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) //
- .to(Constants.FROM_ID_PARAMETER_NAME)
+ .to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(relatedInternalId) //
.to(Constants.TO_ID_PARAMETER_NAME) //
+ .bind(idValue) //
+ .to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) //
.bindAll(statementHolder.getProperties())
.fetchAs(Long.class).one();
- if (idProperty != null) {
+ if (idProperty != null && isNewRelationship) {
relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
.setProperty(idProperty, relationshipInternalId.get());
diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java
index 03e63abdbf..c4e3038b47 100644
--- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java
+++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java
@@ -673,8 +673,14 @@ private Mono processNestedRelations(Neo4jPersistentEntity> sourceEntity,
relatedInternalId);
}
+ Object idValue = idProperty != null
+ ? relationshipContext
+ .getRelationshipPropertiesPropertyAccessor(relatedValueToStore).getProperty(idProperty)
+ : null;
+
+ boolean isNewRelationship = idValue == null;
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatement(
- sourceEntity, relationshipContext, relatedValueToStore);
+ sourceEntity, relationshipContext, relatedValueToStore, isNewRelationship);
// in case of no properties the bind will just return an empty map
Mono relationshipCreationMonoNested = neo4jClient
@@ -683,10 +689,12 @@ private Mono processNestedRelations(Neo4jPersistentEntity> sourceEntity,
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(relatedInternalId) //
.to(Constants.TO_ID_PARAMETER_NAME) //
+ .bind(idValue) //
+ .to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) //
.bindAll(statementHolder.getProperties())
.fetchAs(Long.class).one()
.doOnNext(relationshipInternalId -> {
- if (idProperty != null) {
+ if (idProperty != null && isNewRelationship) {
relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
.setProperty(idProperty, relationshipInternalId);
diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java
index a8a1860b85..84676c6564 100644
--- a/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java
+++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java
@@ -41,6 +41,7 @@ public final class Constants {
public static final String NAME_OF_PROPERTIES_PARAM = "__properties__";
public static final String NAME_OF_STATIC_LABELS_PARAM = "__staticLabels__";
public static final String NAME_OF_ENTITY_LIST_PARAM = "__entities__";
+ public static final String NAME_OF_KNOWN_RELATIONSHIP_PARAM = "__knownRelationShipId__";
public static final String NAME_OF_KNOWN_RELATIONSHIPS_PARAM = "__knownRelationShipIds__";
public static final String NAME_OF_PATHS = "__paths__";
public static final String NAME_OF_ALL_PROPERTIES = "__allProperties__";
diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java
index 1032454ce2..58f78ca3c0 100644
--- a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java
+++ b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java
@@ -364,7 +364,9 @@ public Statement prepareSaveOfRelationship(Neo4jPersistentEntity> neo4jPersist
@NonNull
public Statement prepareSaveOfRelationshipWithProperties(Neo4jPersistentEntity> neo4jPersistentEntity,
- RelationshipDescription relationship, @Nullable String dynamicRelationshipType) {
+ RelationshipDescription relationship,
+ boolean isNew,
+ @Nullable String dynamicRelationshipType) {
Assert.isTrue(relationship.hasRelationshipProperties(),
"Properties required to create a relationship with properties");
@@ -383,11 +385,16 @@ public Statement prepareSaveOfRelationshipWithProperties(Neo4jPersistentEntity
startNode.relationshipFrom(endNode, type))
.named(RELATIONSHIP_NAME);
- return match(startNode)
+ StatementBuilder.OngoingReadingWithWhere startAndEndNodeMatch = match(startNode)
.where(neo4jPersistentEntity.isUsingInternalIds() ? startNode.internalId().isEqualTo(idParameter)
: startNode.property(idPropertyName).isEqualTo(idParameter))
- .match(endNode).where(endNode.internalId().isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME)))
- .merge(relationshipFragment)
+ .match(endNode).where(endNode.internalId().isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME)));
+
+ StatementBuilder.ExposesSet merge = isNew
+ ? startAndEndNodeMatch.create(relationshipFragment)
+ : startAndEndNodeMatch.match(relationshipFragment)
+ .where(Functions.id(relationshipFragment).isEqualTo(Cypher.parameter(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM)));
+ return merge
.mutate(RELATIONSHIP_NAME, relationshipProperties)
.returning(Functions.id(relationshipFragment))
.build();
diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java
index fe0a12df11..b386162ad3 100644
--- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java
+++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java
@@ -320,7 +320,10 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
this.beanFactory = applicationContext.getAutowireCapableBeanFactory();
}
- public CreateRelationshipStatementHolder createStatement(Neo4jPersistentEntity> neo4jPersistentEntity, NestedRelationshipContext relationshipContext, Object relatedValue) {
+ public CreateRelationshipStatementHolder createStatement(Neo4jPersistentEntity> neo4jPersistentEntity,
+ NestedRelationshipContext relationshipContext,
+ Object relatedValue,
+ boolean isNewRelationship) {
if (relationshipContext.hasRelationshipWithProperties()) {
MappingSupport.RelationshipPropertiesWithEntityHolder relatedValueEntityHolder =
@@ -343,7 +346,7 @@ public CreateRelationshipStatementHolder createStatement(Neo4jPersistentEntity
}
return createStatementForRelationShipWithProperties(
neo4jPersistentEntity, relationshipContext,
- dynamicRelationshipType, relatedValueEntityHolder
+ dynamicRelationshipType, relatedValueEntityHolder, isNewRelationship
);
} else {
return createStatementForRelationshipWithoutProperties(neo4jPersistentEntity, relationshipContext, relatedValue);
@@ -351,10 +354,12 @@ public CreateRelationshipStatementHolder createStatement(Neo4jPersistentEntity
}
private CreateRelationshipStatementHolder createStatementForRelationShipWithProperties(Neo4jPersistentEntity> neo4jPersistentEntity,
- NestedRelationshipContext relationshipContext, @Nullable String dynamicRelationshipType, MappingSupport.RelationshipPropertiesWithEntityHolder relatedValue) {
+ NestedRelationshipContext relationshipContext, @Nullable String dynamicRelationshipType,
+ MappingSupport.RelationshipPropertiesWithEntityHolder relatedValue, boolean isNewRelationship) {
Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationshipWithProperties(
- neo4jPersistentEntity, relationshipContext.getRelationship(), dynamicRelationshipType);
+ neo4jPersistentEntity, relationshipContext.getRelationship(), isNewRelationship, dynamicRelationshipType);
+
Map propMap = new HashMap<>();
// write relationship properties
getEntityConverter().write(relatedValue.getRelationshipProperties(), propMap);
diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java
index 92ee620700..21b2018e40 100644
--- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java
+++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java
@@ -2329,6 +2329,33 @@ void saveRelatedEntitesWithSameCustomIdsAndPlainRelationships(
assertThat(list).hasSize(0);
}
}
+
+ @Test // GH-2196
+ void saveSameNodeWithDoubleRelationship(@Autowired HobbyWithRelationshipWithPropertiesRepository repository) {
+ AltHobby hobby = new AltHobby();
+ hobby.setName("Music");
+
+ AltPerson altPerson = new AltPerson("Freddie");
+
+ AltLikedByPersonRelationship rel1 = new AltLikedByPersonRelationship();
+ rel1.setRating(5);
+ rel1.setAltPerson(altPerson);
+
+ AltLikedByPersonRelationship rel2 = new AltLikedByPersonRelationship();
+ rel2.setRating(1);
+ rel2.setAltPerson(altPerson);
+
+ hobby.getLikedBy().add(rel1);
+ hobby.getLikedBy().add(rel2);
+ repository.save(hobby);
+
+ hobby = repository.loadFromCustomQuery(altPerson.getId());
+ assertThat(hobby.getName()).isEqualTo("Music");
+ List likedBy = hobby.getLikedBy();
+ assertThat(likedBy).hasSize(2);
+
+ assertThat(likedBy).containsExactlyInAnyOrder(rel1, rel2);
+ }
}
@Nested
diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java
index e6907519b3..044156236f 100644
--- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java
+++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java
@@ -2085,6 +2085,37 @@ void saveBidirectionalRelationship(@Autowired BidirectionalStartRepository repos
assertThat(records).hasSize(1);
}
}
+
+ @Test // GH-2196
+ void saveSameNodeWithDoubleRelationship(@Autowired ReactiveHobbyWithRelationshipWithPropertiesRepository repository) {
+ AltHobby hobby = new AltHobby();
+ hobby.setName("Music");
+
+ AltPerson altPerson = new AltPerson("Freddie");
+
+ AltLikedByPersonRelationship rel1 = new AltLikedByPersonRelationship();
+ rel1.setRating(5);
+ rel1.setAltPerson(altPerson);
+
+ AltLikedByPersonRelationship rel2 = new AltLikedByPersonRelationship();
+ rel2.setRating(1);
+ rel2.setAltPerson(altPerson);
+
+ hobby.getLikedBy().add(rel1);
+ hobby.getLikedBy().add(rel2);
+ StepVerifier.create(repository.save(hobby))
+ .expectNextCount(1)
+ .verifyComplete();
+
+ StepVerifier.create(repository.loadFromCustomQuery(altPerson.getId()))
+ .assertNext(loadedHobby -> {
+ assertThat(loadedHobby.getName()).isEqualTo("Music");
+ List likedBy = loadedHobby.getLikedBy();
+ assertThat(likedBy).hasSize(2);
+ assertThat(likedBy).containsExactlyInAnyOrder(rel1, rel2);
+ })
+ .verifyComplete();
+ }
}
@Nested