Skip to content

Commit 1a9f7fa

Browse files
GH-2118 - Don't overwrite unknown properties of known nodes and relationships.
This commit fixes the behaviour of overwrwiting unknown properties of nodes and relationships: Instead of using the SET operation we must use MUTATE, effectivly creating a `n += properties` instead of `n = properties` assignment. Furthermore, it requires a change by users: We offered the possibility to use `@RelationshipProperties` without the need for a field storing the internal id. The id however _is_ needed to check whether a relationship already exist or not. While this is irrelevant for relationships without properties, it is very much relevant for the ones having properties. We log a warning now about the fact. That will be a hard requirement for 6.1. To make this work, the verification of the presence of an id property needed to be reworked. It id property is now stored in a thread safe lazy as well and assigned during the creation of a persistent entity. Some minor improvements have been added as well: * We don't pass the literal value for the target id when saving new relationships * We avoid some parameters in the Cypher generator * We optimized a condition in the converter so that it leaves evaluating retrieved values early This closes #2118 and requires Cypher-DSL 2020.1.6.
1 parent 7e9011d commit 1a9f7fa

38 files changed

+1077
-122
lines changed

Diff for: pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
<cdi>2.0</cdi>
7777
<changelist>-SNAPSHOT</changelist>
7878
<checkstyle.version>8.29</checkstyle.version>
79-
<cypher-dsl.version>2020.1.4</cypher-dsl.version>
79+
<cypher-dsl.version>2020.1.6</cypher-dsl.version>
8080
<dist.id>spring-data-neo4j</dist.id>
8181
<dist.key>SDNEO4J</dist.key>
8282
<flatten-maven-plugin.version>1.2.5</flatten-maven-plugin.version>

Diff for: src/main/asciidoc/faq/faq.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@ public final class Person {
288288
@RelationshipProperties
289289
public final class Actor {
290290
291+
@Id @GeneratedValue
292+
private final Long id;
293+
291294
@TargetNode
292295
private final Person person;
293296

Diff for: src/main/asciidoc/object-mapping/mapping.adoc

+4
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ A relationship property class and its usage may look like this:
160160
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/domain/Roles.java[tags=mapping.relationship.properties]
161161
----
162162

163+
You should define a property for the generated, internal ID so that SDN can determine during save which relationships
164+
can be safely overwritten without losing properties.
165+
A generated id property is optional in SDN 6.0.4 and upwards and will be required in SDN 6.1 for `@RelationshipProperties`.
166+
163167
.Defining relationship properties for an entity
164168
[source,java,indent=0]
165169
----

Diff for: src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java

+58-26
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
/**
7070
* @author Michael J. Simons
7171
* @author Philipp Tölle
72+
* @author Gerrit Meier
7273
* @soundtrack Motörhead - We Are Motörhead
7374
* @since 6.0
7475
*/
@@ -237,7 +238,7 @@ private <T> T saveImpl(T instance, @Nullable String inDatabase) {
237238
Optional<Long> optionalInternalId = neo4jClient
238239
.query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels)))
239240
.in(inDatabase)
240-
.bind((T) entityToBeSaved)
241+
.bind(entityToBeSaved)
241242
.with(neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass()))
242243
.fetchAs(Long.class).one();
243244

@@ -443,24 +444,32 @@ private void processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Ob
443444
new NestedRelationshipProcessingStateMachine());
444445
}
445446

446-
private void processNestedRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject,
447+
private void processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, Object parentObject,
447448
boolean isParentObjectNew, @Nullable String inDatabase, NestedRelationshipProcessingStateMachine stateMachine) {
448449

449-
PersistentPropertyAccessor<?> propertyAccessor = neo4jPersistentEntity.getPropertyAccessor(parentObject);
450-
Object fromId = propertyAccessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty());
450+
PersistentPropertyAccessor<?> propertyAccessor = sourceEntity.getPropertyAccessor(parentObject);
451+
Object fromId = propertyAccessor.getProperty(sourceEntity.getRequiredIdProperty());
451452

452-
neo4jPersistentEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
453+
sourceEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
453454

454455
// create context to bundle parameters
455456
NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor,
456-
neo4jPersistentEntity);
457+
sourceEntity);
457458

458459
Collection<?> relatedValuesToStore = MappingSupport.unifyRelationshipValue(relationshipContext.getInverse(),
459460
relationshipContext.getValue());
460461

461462
RelationshipDescription relationshipDescription = relationshipContext.getRelationship();
462463
RelationshipDescription relationshipDescriptionObverse = relationshipDescription.getRelationshipObverse();
463464

465+
Neo4jPersistentProperty idProperty;
466+
if (!relationshipDescription.hasInternalIdProperty()) {
467+
idProperty = null;
468+
} else {
469+
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity();
470+
idProperty = relationshipPropertiesEntity.getIdProperty();
471+
}
472+
464473
// break recursive procession and deletion of previously created relationships
465474
ProcessState processState = stateMachine.getStateOf(relationshipDescriptionObverse, relatedValuesToStore);
466475
if (processState == ProcessState.PROCESSED_ALL_RELATIONSHIPS) {
@@ -470,12 +479,29 @@ private void processNestedRelations(Neo4jPersistentEntity<?> neo4jPersistentEnti
470479
// remove all relationships before creating all new if the entity is not new
471480
// this avoids the usage of cache but might have significant impact on overall performance
472481
if (!isParentObjectNew) {
473-
Statement relationshipRemoveQuery = cypherGenerator.prepareDeleteOf(neo4jPersistentEntity,
474-
relationshipDescription);
482+
483+
List<Long> knownRelationshipsIds = new ArrayList<>();
484+
if (idProperty != null) {
485+
for (Object relatedValueToStore : relatedValuesToStore) {
486+
if (relatedValueToStore == null) {
487+
continue;
488+
}
489+
490+
Long id = (Long) relationshipContext.getRelationshipPropertiesPropertyAccessor(relatedValueToStore).getProperty(idProperty);
491+
if (id != null) {
492+
knownRelationshipsIds.add(id);
493+
}
494+
}
495+
}
496+
497+
Statement relationshipRemoveQuery = cypherGenerator.prepareDeleteOf(sourceEntity, relationshipDescription);
475498

476499
neo4jClient.query(renderer.render(relationshipRemoveQuery)).in(inDatabase)
477-
.bind(convertIdValues(neo4jPersistentEntity.getIdProperty(), fromId))
478-
.to(Constants.FROM_ID_PARAMETER_NAME).run();
500+
.bind(convertIdValues(sourceEntity.getIdProperty(), fromId)) //
501+
.to(Constants.FROM_ID_PARAMETER_NAME) //
502+
.bind(knownRelationshipsIds) //
503+
.to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) //
504+
.run();
479505
}
480506

481507
// nothing to do because there is nothing to map
@@ -489,33 +515,39 @@ private void processNestedRelations(Neo4jPersistentEntity<?> neo4jPersistentEnti
489515

490516
// here a map entry is not always anymore a dynamic association
491517
Object relatedNode = relationshipContext.identifyAndExtractRelationshipTargetNode(relatedValueToStore);
492-
Neo4jPersistentEntity<?> targetNodeDescription = neo4jMappingContext
493-
.getPersistentEntity(relatedNode.getClass());
518+
Neo4jPersistentEntity<?> targetEntity = neo4jMappingContext.getPersistentEntity(relatedNode.getClass());
494519

495-
boolean isEntityNew = targetNodeDescription.isNew(relatedNode);
520+
boolean isEntityNew = targetEntity.isNew(relatedNode);
496521

497522
relatedNode = eventSupport.maybeCallBeforeBind(relatedNode);
498523

499524
Long relatedInternalId = saveRelatedNode(relatedNode, relationshipContext.getAssociationTargetType(),
500-
targetNodeDescription, inDatabase);
525+
targetEntity, inDatabase);
501526

502527
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatement(
503-
neo4jPersistentEntity, relationshipContext, relatedInternalId, relatedValueToStore);
504-
505-
neo4jClient.query(renderer.render(statementHolder.getStatement())).in(inDatabase)
506-
.bind(convertIdValues(targetNodeDescription.getRequiredIdProperty(), fromId))
507-
.to(Constants.FROM_ID_PARAMETER_NAME).bindAll(statementHolder.getProperties())
508-
.run();
528+
sourceEntity, relationshipContext, relatedValueToStore);
529+
530+
Optional<Long> relationshipInternalId = neo4jClient.query(renderer.render(statementHolder.getStatement())).in(inDatabase)
531+
.bind(convertIdValues(targetEntity.getRequiredIdProperty(), fromId)) //
532+
.to(Constants.FROM_ID_PARAMETER_NAME)
533+
.bind(relatedInternalId) //
534+
.to(Constants.TO_ID_PARAMETER_NAME) //
535+
.bindAll(statementHolder.getProperties())
536+
.fetchAs(Long.class).one();
537+
538+
if (idProperty != null) {
539+
relationshipContext
540+
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
541+
.setProperty(idProperty, relationshipInternalId.get());
542+
}
509543

510544
// if an internal id is used this must get set to link this entity in the next iteration
511-
if (targetNodeDescription.isUsingInternalIds()) {
512-
PersistentPropertyAccessor<?> targetPropertyAccessor = targetNodeDescription
513-
.getPropertyAccessor(relatedNode);
514-
targetPropertyAccessor
515-
.setProperty(targetNodeDescription.getRequiredIdProperty(), relatedInternalId);
545+
if (targetEntity.isUsingInternalIds()) {
546+
PersistentPropertyAccessor<?> targetPropertyAccessor = targetEntity.getPropertyAccessor(relatedNode);
547+
targetPropertyAccessor.setProperty(targetEntity.getRequiredIdProperty(), relatedInternalId);
516548
}
517549
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
518-
processNestedRelations(targetNodeDescription, relatedNode, isEntityNew, inDatabase, stateMachine);
550+
processNestedRelations(targetEntity, relatedNode, isEntityNew, inDatabase, stateMachine);
519551
}
520552
}
521553
});

Diff for: src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java

+65-29
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import org.neo4j.cypherdsl.core.Statement;
4141
import org.neo4j.cypherdsl.core.renderer.Renderer;
4242
import org.neo4j.driver.exceptions.NoSuchRecordException;
43-
import org.neo4j.driver.summary.ResultSummary;
4443
import org.neo4j.driver.summary.SummaryCounters;
4544
import org.springframework.beans.BeansException;
4645
import org.springframework.beans.factory.BeanFactory;
@@ -232,7 +231,7 @@ private <T> Mono<T> saveImpl(T instance, @Nullable String inDatabase) {
232231
Statement saveStatement = cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels);
233232

234233
Mono<Long> idMono = this.neo4jClient.query(() -> renderer.render(saveStatement)).in(inDatabase)
235-
.bind((T) entity).with(neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entity.getClass()))
234+
.bind(entity).with(neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entity.getClass()))
236235
.fetchAs(Long.class).one().switchIfEmpty(Mono.defer(() -> {
237236
if (entityMetaData.hasVersionProperty()) {
238237
return Mono.error(() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE));
@@ -440,26 +439,34 @@ private Mono<Void> processRelations(Neo4jPersistentEntity<?> neo4jPersistentEnti
440439
new NestedRelationshipProcessingStateMachine());
441440
}
442441

443-
private Mono<Void> processNestedRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject,
442+
private Mono<Void> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, Object parentObject,
444443
boolean isParentObjectNew, @Nullable String inDatabase, NestedRelationshipProcessingStateMachine stateMachine) {
445444

446445
return Mono.defer(() -> {
447-
PersistentPropertyAccessor<?> propertyAccessor = neo4jPersistentEntity.getPropertyAccessor(parentObject);
448-
Object fromId = propertyAccessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty());
446+
PersistentPropertyAccessor<?> propertyAccessor = sourceEntity.getPropertyAccessor(parentObject);
447+
Object fromId = propertyAccessor.getProperty(sourceEntity.getRequiredIdProperty());
449448
List<Mono<Void>> relationshipCreationMonos = new ArrayList<>();
450449

451-
neo4jPersistentEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
450+
sourceEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
452451

453452
// create context to bundle parameters
454453
NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor,
455-
neo4jPersistentEntity);
454+
sourceEntity);
456455

457456
Collection<?> relatedValuesToStore = MappingSupport.unifyRelationshipValue(relationshipContext.getInverse(),
458457
relationshipContext.getValue());
459458

460459
RelationshipDescription relationshipDescription = relationshipContext.getRelationship();
461460
RelationshipDescription relationshipDescriptionObverse = relationshipDescription.getRelationshipObverse();
462461

462+
Neo4jPersistentProperty idProperty;
463+
if (!relationshipDescription.hasInternalIdProperty()) {
464+
idProperty = null;
465+
} else {
466+
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity();
467+
idProperty = relationshipPropertiesEntity.getIdProperty();
468+
}
469+
463470
// break recursive procession and deletion of previously created relationships
464471
ProcessState processState = stateMachine.getStateOf(relationshipDescriptionObverse, relatedValuesToStore);
465472
if (processState == ProcessState.PROCESSED_ALL_RELATIONSHIPS) {
@@ -469,13 +476,32 @@ private Mono<Void> processNestedRelations(Neo4jPersistentEntity<?> neo4jPersiste
469476
// remove all relationships before creating all new if the entity is not new
470477
// this avoids the usage of cache but might have significant impact on overall performance
471478
if (!isParentObjectNew) {
472-
Statement relationshipRemoveQuery = cypherGenerator.prepareDeleteOf(neo4jPersistentEntity,
473-
relationshipDescription);
479+
480+
List<Long> knownRelationshipsIds = new ArrayList<>();
481+
if (idProperty != null) {
482+
for (Object relatedValueToStore : relatedValuesToStore) {
483+
if (relatedValueToStore == null) {
484+
continue;
485+
}
486+
487+
Long id = (Long) relationshipContext
488+
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
489+
.getProperty(idProperty);
490+
if (id != null) {
491+
knownRelationshipsIds.add(id);
492+
}
493+
}
494+
}
495+
496+
Statement relationshipRemoveQuery = cypherGenerator.prepareDeleteOf(sourceEntity, relationshipDescription);
474497

475498
relationshipCreationMonos.add(
476499
neo4jClient.query(renderer.render(relationshipRemoveQuery)).in(inDatabase)
477-
.bind(convertIdValues(neo4jPersistentEntity.getIdProperty(), fromId))
478-
.to(Constants.FROM_ID_PARAMETER_NAME).run().checkpoint("delete relationships").then());
500+
.bind(convertIdValues(sourceEntity.getIdProperty(), fromId)) //
501+
.to(Constants.FROM_ID_PARAMETER_NAME) //
502+
.bind(knownRelationshipsIds) //
503+
.to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) //
504+
.run().checkpoint("delete relationships").then());
479505
}
480506

481507
// nothing to do because there is nothing to map
@@ -487,37 +513,47 @@ private Mono<Void> processNestedRelations(Neo4jPersistentEntity<?> neo4jPersiste
487513

488514
for (Object relatedValueToStore : relatedValuesToStore) {
489515

490-
Object valueToBeSavedPreEvt = relationshipContext.identifyAndExtractRelationshipTargetNode(relatedValueToStore);
516+
Object relatedNodePreEvt = relationshipContext.identifyAndExtractRelationshipTargetNode(relatedValueToStore);
491517

492-
Mono<Void> createRelationship = eventSupport.maybeCallBeforeBind(valueToBeSavedPreEvt)
493-
.flatMap(valueToBeSaved -> {
494-
Neo4jPersistentEntity<?> targetNodeDescription = neo4jMappingContext
495-
.getPersistentEntity(valueToBeSavedPreEvt.getClass());
496-
return Mono.just(targetNodeDescription.isNew(valueToBeSaved)).flatMap(isNew ->
497-
saveRelatedNode(valueToBeSaved, relationshipContext.getAssociationTargetType(),
498-
targetNodeDescription, inDatabase).flatMap(relatedInternalId -> {
518+
Mono<Void> createRelationship = eventSupport.maybeCallBeforeBind(relatedNodePreEvt)
519+
.flatMap(relatedNode -> {
520+
Neo4jPersistentEntity<?> targetEntity = neo4jMappingContext
521+
.getPersistentEntity(relatedNodePreEvt.getClass());
522+
return Mono.just(targetEntity.isNew(relatedNode)).flatMap(isNew ->
523+
saveRelatedNode(relatedNode, relationshipContext.getAssociationTargetType(),
524+
targetEntity, inDatabase).flatMap(relatedInternalId -> {
499525

500526
// if an internal id is used this must get set to link this entity in the next iteration
501-
if (targetNodeDescription.isUsingInternalIds()) {
502-
PersistentPropertyAccessor<?> targetPropertyAccessor = targetNodeDescription
503-
.getPropertyAccessor(valueToBeSaved);
504-
targetPropertyAccessor.setProperty(targetNodeDescription.getRequiredIdProperty(),
527+
if (targetEntity.isUsingInternalIds()) {
528+
PersistentPropertyAccessor<?> targetPropertyAccessor = targetEntity
529+
.getPropertyAccessor(relatedNode);
530+
targetPropertyAccessor.setProperty(targetEntity.getRequiredIdProperty(),
505531
relatedInternalId);
506532
}
507533

508534
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatement(
509-
neo4jPersistentEntity, relationshipContext, relatedInternalId, relatedValueToStore);
535+
sourceEntity, relationshipContext, relatedValueToStore);
510536

511537
// in case of no properties the bind will just return an empty map
512-
Mono<ResultSummary> relationshipCreationMonoNested = neo4jClient
538+
Mono<Long> relationshipCreationMonoNested = neo4jClient
513539
.query(renderer.render(statementHolder.getStatement())).in(inDatabase)
514-
.bind(convertIdValues(targetNodeDescription.getRequiredIdProperty(), fromId))
515-
.to(Constants.FROM_ID_PARAMETER_NAME)
516-
.bindAll(statementHolder.getProperties()).run();
540+
.bind(convertIdValues(targetEntity.getRequiredIdProperty(), fromId)) //
541+
.to(Constants.FROM_ID_PARAMETER_NAME) //
542+
.bind(relatedInternalId) //
543+
.to(Constants.TO_ID_PARAMETER_NAME) //
544+
.bindAll(statementHolder.getProperties())
545+
.fetchAs(Long.class).one()
546+
.doOnNext(relationshipInternalId -> {
547+
if (idProperty != null) {
548+
relationshipContext
549+
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
550+
.setProperty(idProperty, relationshipInternalId);
551+
}
552+
});
517553

518554
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
519555
return relationshipCreationMonoNested.checkpoint().then(
520-
processNestedRelations(targetNodeDescription, valueToBeSaved,
556+
processNestedRelations(targetEntity, relatedNode,
521557
isNew, inDatabase, stateMachine));
522558
} else {
523559
return relationshipCreationMonoNested.checkpoint().then();

Diff for: src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public final class Constants {
5252
*/
5353
public static final String NAME_OF_STATIC_LABELS_PARAM = "__staticLabels__";
5454
public static final String NAME_OF_ENTITY_LIST_PARAM = "__entities__";
55+
public static final String NAME_OF_KNOWN_RELATIONSHIPS_PARAM = "__knownRelationShipIds__";
5556
public static final String NAME_OF_PATHS = "__paths__";
5657
public static final String NAME_OF_ALL_PROPERTIES = "__allProperties__";
5758

@@ -60,6 +61,7 @@ public final class Constants {
6061
public static final String NAME_OF_SYNTHESIZED_RELATIONS = "__sr__";
6162

6263
public static final String FROM_ID_PARAMETER_NAME = "fromId";
64+
public static final String TO_ID_PARAMETER_NAME = "toId";
6365

6466
private Constants() {
6567
}

0 commit comments

Comments
 (0)