Skip to content

Commit 3ed5c70

Browse files
feat: Allow bidirectional mapping of relationship with properties. (spring-projects#2914)
This will allow for the mapper to have only *one* physical relationship plus the original behaviour staying intact (creating two independent). Required mapping is shown in the test. Basic idea is to check if a relationship in the opposite direction with the actual *same* source and target entities has already been seen. If so, no batch update on the imperative path is scheduled. Thus however will leave generated ids on the mapping classes unpopulated. Those will be retrieved after the fact. --------- Co-authored-by: Gerrit Meier <meistermeier@gmail.com>
1 parent cd068af commit 3ed5c70

28 files changed

+1438
-227
lines changed

src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,7 @@ private <T> T processNestedRelations(
883883
List<Map<String, Object>> relationshipPropertiesRows = new ArrayList<>();
884884
List<Map<String, Object>> newRelationshipPropertiesRows = new ArrayList<>();
885885
List<Object> updateRelatedValuesToStore = new ArrayList<>();
886-
List<Object> newRelatedValuesToStore = new ArrayList<>();
886+
List<Object> newRelationshipPropertiesToStore = new ArrayList<>();
887887

888888
for (Object relatedValueToStore : relatedValuesToStore) {
889889

@@ -977,15 +977,22 @@ private <T> T processNestedRelations(
977977
.bindAll(statementHolder.getProperties())
978978
.run();
979979
}
980-
} else if (relationshipDescription.hasRelationshipProperties() && isNewRelationship && idProperty != null) {
981-
newRelationshipPropertiesRows.add(properties);
982-
newRelatedValuesToStore.add(relatedValueToStore);
983980
} else if (relationshipDescription.hasRelationshipProperties()) {
984-
neo4jMappingContext.getEntityConverter().write(
985-
((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore).getRelationshipProperties(),
986-
properties);
987-
988-
relationshipPropertiesRows.add(properties);
981+
// check if bidi mapped already
982+
var hlp = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore);
983+
var hasProcessedRelationshipEntity = stateMachine.hasProcessedRelationshipEntity(propertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship());
984+
if (hasProcessedRelationshipEntity) {
985+
stateMachine.requireIdUpdate(sourceEntity, relationshipDescription, canUseElementId, fromId, relatedInternalId, relationshipContext, relatedValueToStore, idProperty);
986+
} else {
987+
if (isNewRelationship && idProperty != null) {
988+
newRelationshipPropertiesRows.add(properties);
989+
newRelationshipPropertiesToStore.add(relatedValueToStore);
990+
} else {
991+
neo4jMappingContext.getEntityConverter().write(hlp.getRelationshipProperties(), properties);
992+
relationshipPropertiesRows.add(properties);
993+
}
994+
stateMachine.storeProcessRelationshipEntity(hlp, propertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship());
995+
}
989996
} else {
990997
// non-dynamic relationship or relationship with properties
991998
plainRelationshipRows.add(properties);
@@ -1020,9 +1027,9 @@ private <T> T processNestedRelations(
10201027
.bindAll(statementHolder.getProperties())
10211028
.run();
10221029
}
1023-
if (!newRelatedValuesToStore.isEmpty()) {
1030+
if (!newRelationshipPropertiesToStore.isEmpty()) {
10241031
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForImperativeRelationshipsWithPropertiesBatch(true,
1025-
sourceEntity, relationshipDescription, newRelatedValuesToStore, newRelationshipPropertiesRows, canUseElementId);
1032+
sourceEntity, relationshipDescription, newRelationshipPropertiesToStore, newRelationshipPropertiesRows, canUseElementId);
10261033
List<Object> all = new ArrayList<>(neo4jClient.query(renderer.render(statementHolder.getStatement()))
10271034
.bindAll(statementHolder.getProperties())
10281035
.fetchAs(Object.class)
@@ -1031,11 +1038,14 @@ private <T> T processNestedRelations(
10311038
// assign new ids
10321039
for (int i = 0; i < all.size(); i++) {
10331040
Object anId = all.get(i);
1034-
assignIdToRelationshipProperties(relationshipContext, newRelatedValuesToStore.get(i), idProperty, anId);
1041+
assignIdToRelationshipProperties(relationshipContext, newRelationshipPropertiesToStore.get(i), idProperty, anId);
10351042
}
10361043
}
10371044
}
10381045

1046+
// Possible grab missing relationship ids now for bidirectional ones, with properties, mapped in opposite directions
1047+
stateMachine.updateRelationshipIds(this::getRelationshipId);
1048+
10391049
relationshipHandler.applyFinalResultToOwner(propertyAccessor);
10401050
});
10411051

@@ -1044,6 +1054,19 @@ private <T> T processNestedRelations(
10441054
return finalSubgraphRoot;
10451055
}
10461056

1057+
private Optional<Object> getRelationshipId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId) {
1058+
1059+
return neo4jClient.query(renderer.render(statement))
1060+
.bind(convertIdValues(idProperty, fromId)) //
1061+
.to(Constants.FROM_ID_PARAMETER_NAME) //
1062+
.bind(toId) //
1063+
.to(Constants.TO_ID_PARAMETER_NAME) //
1064+
.fetchAs(Object.class)
1065+
.mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r))
1066+
.one();
1067+
}
1068+
1069+
10471070
// The pendant to {@link #saveRelatedNode(Object, NodeDescription, PropertyFilter, PropertyFilter.RelaxedPropertyPath)}
10481071
// We can't do without a query, as we need to refresh the internal id
10491072
private Entity loadRelatedNode(NodeDescription<?> targetNodeDescription, Object relatedInternalId) {

src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java

Lines changed: 87 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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) {

src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,8 @@ public Statement prepareSaveOfRelationshipWithProperties(Neo4jPersistentEntity<?
541541
RelationshipDescription relationship,
542542
boolean isNew,
543543
@Nullable String dynamicRelationshipType,
544-
boolean canUseElementId) {
544+
boolean canUseElementId,
545+
boolean matchOnly) {
545546

546547
Assert.isTrue(relationship.hasRelationshipProperties(),
547548
"Properties required to create a relationship with properties");
@@ -567,6 +568,12 @@ public Statement prepareSaveOfRelationshipWithProperties(Neo4jPersistentEntity<?
567568
.match(endNode)
568569
.where(getEndNodeIdFunction((Neo4jPersistentEntity<?>) relationship.getTarget(), canUseElementId).apply(endNode).isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME)));
569570

571+
if (matchOnly) {
572+
return startAndEndNodeMatch.match(relationshipFragment)
573+
.returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment))
574+
.build();
575+
}
576+
570577
StatementBuilder.ExposesSet createOrMatch = isNew
571578
? startAndEndNodeMatch.create(relationshipFragment)
572579
: startAndEndNodeMatch.match(relationshipFragment)

src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ private CreateRelationshipStatementHolder createStatementForRelationshipWithProp
603603

604604
Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationshipWithProperties(
605605
neo4jPersistentEntity, relationshipDescription, isNewRelationship,
606-
dynamicRelationshipType, canUseElementId);
606+
dynamicRelationshipType, canUseElementId, false);
607607

608608
Map<String, Object> propMap = new HashMap<>();
609609
// write relationship properties

0 commit comments

Comments
 (0)