Skip to content

Don't override relationships if they are different. #2198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>6.0.7-SNAPSHOT</version>
<version>6.0.7-GH-2196-SNAPSHOT</version>

<name>Spring Data Neo4j</name>
<description>Next generation Object-Graph-Mapping for Spring Data.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,18 +531,27 @@ private <T> 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<Long> 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense in the Cypher prepareSaveOfRelationshipWithProperties, here I am wondering a bit if that should be || isNewRelationship

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes only sense to update the id value if it was not set before. It should not change, or? (please say no ;) )

relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
.setProperty(idProperty, relationshipInternalId.get());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -673,8 +673,14 @@ private Mono<Void> 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<Long> relationshipCreationMonoNested = neo4jClient
Expand All @@ -683,10 +689,12 @@ private Mono<Void> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -343,18 +346,20 @@ public CreateRelationshipStatementHolder createStatement(Neo4jPersistentEntity<?
}
return createStatementForRelationShipWithProperties(
neo4jPersistentEntity, relationshipContext,
dynamicRelationshipType, relatedValueEntityHolder
dynamicRelationshipType, relatedValueEntityHolder, isNewRelationship
);
} else {
return createStatementForRelationshipWithoutProperties(neo4jPersistentEntity, relationshipContext, relatedValue);
}
}

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<String, Object> propMap = new HashMap<>();
// write relationship properties
getEntityConverter().write(relatedValue.getRelationshipProperties(), propMap);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AltLikedByPersonRelationship> likedBy = hobby.getLikedBy();
assertThat(likedBy).hasSize(2);

assertThat(likedBy).containsExactlyInAnyOrder(rel1, rel2);
}
}

@Nested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AltLikedByPersonRelationship> likedBy = loadedHobby.getLikedBy();
assertThat(likedBy).hasSize(2);
assertThat(likedBy).containsExactlyInAnyOrder(rel1, rel2);
})
.verifyComplete();
}
}

@Nested
Expand Down