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 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 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