Skip to content

Commit d8bff18

Browse files
refactor: Avoid all usage of Neo4j id() function unless required by the users datamodel.
This change makes use of the Cypher-DSL dialect capatibilities and uses a String identifier internally for all objects. This will be the `elementId()` on Neo4j 5 and higher and `toString(id())` on Neo4j 4 and lower. Unless explicitly configured by the user through having `@Id @GeneratedValue long id`, we don’t use the `id()` function anymore. This function has been deprecated in Neo4j 5. Users are encouraged to use `@Id @GeneratedValue String id` for assigning the element id as object id or using custom unique identifiers or `@Id @GeneratedValue UUID id` or `@Id @GeneratedValue(value = UUIDStringGenerator.class) String id` for UUIDs. We marked the long ids as deprecated, but don’t have any plans for now to remove them, so that future SDN versions will still be compatible with older Neo4j versions if the data model contains long ids. We opted for using String’s directly to avoid tons of additional instances and rely on the JVMs excellent String caching capatiblities, thus, the `ElementId` interface - not usable so far anyhow - has been removed without current or planned replacement. See #2716 and other tickets, closes #2718.
1 parent fca0a9a commit d8bff18

31 files changed

+1760
-583
lines changed

etc/migrate-to-element-id.yml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
# Run with
3+
# ./mvnw org.openrewrite.maven:rewrite-maven-plugin:dryRun \
4+
# -Drewrite.checkstyleDetectionEnabled=false \
5+
# -Drewrite.configLocation=etc/migrate-to-element-id.yml \
6+
# -Drewrite.activeRecipes=sdn.elementId.rewriteIdCalls
7+
---
8+
type: specs.openrewrite.org/v1beta/recipe
9+
name: sdn.elementId.rewriteIdCalls
10+
displayName: Change calls to Functions.id to Functions.elemenetId
11+
recipeList:
12+
- org.openrewrite.java.ChangeMethodName:
13+
methodPattern: org.neo4j.cypherdsl.core.Functions id(..)
14+
newMethodName: elementId
15+
ignoreDefinition: true

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

+81-84
Large diffs are not rendered by default.

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

+56-70
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.util.List;
3737
import java.util.Map;
3838
import java.util.Objects;
39+
import java.util.Optional;
3940
import java.util.Set;
4041
import java.util.concurrent.ConcurrentHashMap;
4142
import java.util.concurrent.atomic.AtomicReference;
@@ -446,10 +447,11 @@ private <T> Mono<T> saveImpl(T instance, @Nullable Collection<PropertyFilter.Pro
446447

447448
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
448449
return idMono.doOnNext(newOrUpdatedNode -> {
449-
IdentitySupport.updateInternalId(entityMetaData, propertyAccessor, newOrUpdatedNode);
450+
var elementId = IdentitySupport.getElementId(newOrUpdatedNode);
451+
TemplateSupport.setGeneratedIdIfNecessary(entityMetaData, propertyAccessor, elementId, Optional.of(newOrUpdatedNode));
450452
TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode);
451-
finalStateMachine.markValueAsProcessed(instance, IdentitySupport.getInternalId(newOrUpdatedNode));
452-
}).map(IdentitySupport::getInternalId)
453+
finalStateMachine.markEntityAsProcessed(instance, elementId);
454+
}).map(IdentitySupport::getElementId)
453455
.flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, finalStateMachine, binderFunction.filter));
454456
});
455457
}
@@ -582,15 +584,15 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Collection<Prop
582584
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
583585
.bind(boundedEntityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM)
584586
.fetchAs(Tuple2.class)
585-
.mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_INTERNAL_ID).asLong()))
587+
.mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_ELEMENT_ID).asString()))
586588
.all()
587-
.collectMap(m -> (Value) m.getT1(), m -> (Long) m.getT2());
589+
.collectMap(m -> (Value) m.getT1(), m -> (String) m.getT2());
588590
}).flatMapMany(idToInternalIdMapping -> Flux.fromIterable(entitiesToBeSaved)
589591
.flatMap(t -> {
590592
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(t.getT3());
591593
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
592594
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
593-
Long internalId = idToInternalIdMapping.get(id);
595+
String internalId = idToInternalIdMapping.get(id);
594596
return processRelations(entityMetaData, propertyAccessor, t.getT2(), new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.getT1(), internalId),
595597
TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData));
596598
}))
@@ -709,9 +711,9 @@ private Mono<NodesAndRelationshipsByIdStatementProvider> createNodesAndRelations
709711
return Mono.deferContextual(ctx -> {
710712
Class<?> rootClass = entityMetaData.getUnderlyingClass();
711713

712-
Set<Long> rootNodeIds = ctx.get("rootNodes");
713-
Set<Long> processedRelationshipIds = ctx.get("processedRelationships");
714-
Set<Long> processedNodeIds = ctx.get("processedNodes");
714+
Set<String> rootNodeIds = ctx.get("rootNodes");
715+
Set<String> processedRelationshipIds = ctx.get("processedRelationships");
716+
Set<String> processedNodeIds = ctx.get("processedNodes");
715717
return Flux.fromIterable(entityMetaData.getRelationshipsInHierarchy(queryFragments::includeField))
716718
.flatMap(relationshipDescription -> {
717719

@@ -723,16 +725,19 @@ private Mono<NodesAndRelationshipsByIdStatementProvider> createNodesAndRelations
723725
usedParameters.putAll(statement.getCatalog().getParameters());
724726
return neo4jClient.query(renderer.render(statement))
725727
.bindAll(usedParameters)
726-
.fetchAs(TupleOfLongsHolder.class)
728+
.fetchAs(Tuple2.class)
727729
.mappedBy((t, r) -> {
728-
Collection<Long> rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asList(Value::asLong);
730+
Collection<String> rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asList(Value::asString);
729731
rootNodeIds.addAll(rootIds);
730-
Collection<Long> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asLong);
731-
Collection<Long> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asLong);
732-
return TupleOfLongsHolder.with(Tuples.of(newRelationshipIds, newRelatedNodeIds));
732+
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asString);
733+
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asString);
734+
return Tuples.of(newRelationshipIds, newRelatedNodeIds);
733735
})
734736
.one()
735-
.map(TupleOfLongsHolder::get)
737+
.map((t) -> {
738+
//noinspection unchecked
739+
return (Tuple2<Collection<String>, Collection<String>>) t;
740+
})
736741
.expand(iterateAndMapNextLevel(relationshipDescription, queryFragments, rootClass, PropertyPathWalkStep.empty()));
737742
})
738743
.then(Mono.fromSupplier(() -> new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, processedRelationshipIds, processedNodeIds, queryFragments)));
@@ -744,23 +749,7 @@ private Mono<NodesAndRelationshipsByIdStatementProvider> createNodesAndRelations
744749

745750
}
746751

747-
static class TupleOfLongsHolder {
748-
private final Tuple2<Collection<Long>, Collection<Long>> content;
749-
750-
static TupleOfLongsHolder with(Tuple2<Collection<Long>, Collection<Long>> content) {
751-
return new TupleOfLongsHolder(content);
752-
}
753-
754-
private TupleOfLongsHolder(Tuple2<Collection<Long>, Collection<Long>> content) {
755-
this.content = content;
756-
}
757-
758-
Tuple2<Collection<Long>, Collection<Long>> get() {
759-
return content;
760-
}
761-
}
762-
763-
private Flux<Tuple2<Collection<Long>, Collection<Long>>> iterateNextLevel(Collection<Long> relatedNodeIds,
752+
private Flux<Tuple2<Collection<String>, Collection<String>>> iterateNextLevel(Collection<String> relatedNodeIds,
764753
RelationshipDescription sourceRelationshipDescription, QueryFragments queryFragments,
765754
Class<?> rootClass, PropertyPathWalkStep currentPathStep) {
766755

@@ -786,43 +775,46 @@ private Flux<Tuple2<Collection<Long>, Collection<Long>>> iterateNextLevel(Collec
786775

787776
Statement statement = cypherGenerator
788777
.prepareMatchOf(target, relDe, null,
789-
Functions.id(node).in(Cypher.parameter(Constants.NAME_OF_ID)))
778+
Functions.elementId(node).in(Cypher.parameter(Constants.NAME_OF_ID)))
790779
.returning(cypherGenerator.createGenericReturnStatement()).build();
791780

792781
return neo4jClient.query(renderer.render(statement))
793782
.bindAll(Collections.singletonMap(Constants.NAME_OF_ID, relatedNodeIds))
794-
.fetchAs(TupleOfLongsHolder.class)
783+
.fetchAs(Tuple2.class)
795784
.mappedBy((t, r) -> {
796-
Collection<Long> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asLong);
797-
Collection<Long> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asLong);
785+
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asString);
786+
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asString);
798787

799-
return TupleOfLongsHolder.with(Tuples.of(newRelationshipIds, newRelatedNodeIds));
788+
return Tuples.of(newRelationshipIds, newRelatedNodeIds);
800789
})
801790
.one()
802-
.map(TupleOfLongsHolder::get)
791+
.map((t) -> {
792+
//noinspection unchecked
793+
return (Tuple2<Collection<String>, Collection<String>>) t;
794+
})
803795
.expand(object -> iterateAndMapNextLevel(relDe, queryFragments, rootClass, nextPathStep).apply(object));
804796
});
805797

806798
}
807799

808800
@NonNull
809-
private Function<Tuple2<Collection<Long>, Collection<Long>>,
810-
Publisher<Tuple2<Collection<Long>, Collection<Long>>>> iterateAndMapNextLevel(
801+
private Function<Tuple2<Collection<String>, Collection<String>>,
802+
Publisher<Tuple2<Collection<String>, Collection<String>>>> iterateAndMapNextLevel(
811803
RelationshipDescription relationshipDescription, QueryFragments queryFragments, Class<?> rootClass, PropertyPathWalkStep currentPathStep) {
812804

813805
return newRelationshipAndRelatedNodeIds ->
814806
Flux.deferContextual(ctx -> {
815-
Set<Long> relationshipIds = ctx.get("processedRelationships");
816-
Set<Long> processedNodeIds = ctx.get("processedNodes");
807+
Set<String> relationshipIds = ctx.get("processedRelationships");
808+
Set<String> processedNodeIds = ctx.get("processedNodes");
817809

818-
Collection<Long> newRelationshipIds = newRelationshipAndRelatedNodeIds.getT1();
819-
Set<Long> tmpProcessedRels = ConcurrentHashMap.newKeySet(newRelationshipIds.size());
810+
Collection<String> newRelationshipIds = newRelationshipAndRelatedNodeIds.getT1();
811+
Set<String> tmpProcessedRels = ConcurrentHashMap.newKeySet(newRelationshipIds.size());
820812
tmpProcessedRels.addAll(newRelationshipIds);
821813
tmpProcessedRels.removeAll(relationshipIds);
822814
relationshipIds.addAll(newRelationshipIds);
823815

824-
Collection<Long> newRelatedNodeIds = newRelationshipAndRelatedNodeIds.getT2();
825-
Set<Long> tmpProcessedNodes = ConcurrentHashMap.newKeySet(newRelatedNodeIds.size());
816+
Collection<String> newRelatedNodeIds = newRelationshipAndRelatedNodeIds.getT2();
817+
Set<String> tmpProcessedNodes = ConcurrentHashMap.newKeySet(newRelatedNodeIds.size());
826818
tmpProcessedNodes.addAll(newRelatedNodeIds);
827819
tmpProcessedNodes.removeAll(processedNodeIds);
828820
processedNodeIds.addAll(newRelatedNodeIds);
@@ -904,14 +896,14 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
904896
// This avoids the usage of cache but might have significant impact on overall performance
905897
if (!isParentObjectNew && !stateMachine.hasProcessedRelationship(fromId, relationshipDescription)) {
906898

907-
List<Long> knownRelationshipsIds = new ArrayList<>();
899+
List<Object> knownRelationshipsIds = new ArrayList<>();
908900
if (idProperty != null) {
909901
for (Object relatedValueToStore : relatedValuesToStore) {
910902
if (relatedValueToStore == null) {
911903
continue;
912904
}
913905

914-
Long id = (Long) relationshipContext
906+
Object id = relationshipContext
915907
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
916908
.getProperty(idProperty);
917909
if (id != null) {
@@ -950,44 +942,38 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
950942
.flatMap(newRelatedObject -> {
951943
Neo4jPersistentEntity<?> targetEntity = neo4jMappingContext.getRequiredPersistentEntity(relatedObjectBeforeCallbacksApplied.getClass());
952944

953-
Mono<Tuple2<AtomicReference<Long>, AtomicReference<Entity>>> queryOrSave;
945+
Mono<Tuple2<AtomicReference<String>, AtomicReference<Entity>>> queryOrSave;
954946
if (stateMachine.hasProcessedValue(relatedValueToStore)) {
955-
AtomicReference<Long> relatedInternalId = new AtomicReference<>();
956-
Long possibleValue = stateMachine.getInternalId(relatedValueToStore);
947+
AtomicReference<String> relatedInternalId = new AtomicReference<>();
948+
String possibleValue = stateMachine.getObjectId(relatedValueToStore);
957949
if (possibleValue != null) {
958950
relatedInternalId.set(possibleValue);
959951
}
960952
queryOrSave = Mono.just(Tuples.of(relatedInternalId, new AtomicReference<>()));
961953
} else {
962954
queryOrSave = saveRelatedNode(newRelatedObject, targetEntity, includeProperty, currentPropertyPath)
963-
.map(entity -> Tuples.of(new AtomicReference<>(IdentitySupport.getInternalId(entity)), new AtomicReference<>(entity)))
964-
.doOnNext(entity -> {
965-
stateMachine.markValueAsProcessed(relatedValueToStore, entity.getT1().get());
955+
.map(entity -> Tuples.of(new AtomicReference<>(IdentitySupport.getElementId(entity)), new AtomicReference<>(entity)))
956+
.doOnNext(t -> {
957+
var relatedInternalId = t.getT1().get();
958+
stateMachine.markEntityAsProcessed(relatedValueToStore, relatedInternalId);
966959
if (relatedValueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) {
967-
Object value = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore).getRelatedEntity();
968-
stateMachine.markValueAsProcessedAs(value, entity.getT1().get());
960+
Object entity = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore).getRelatedEntity();
961+
stateMachine.markAsAliased(entity, relatedInternalId);
969962
}
970963
});
971964
}
972965
return queryOrSave.flatMap(idAndEntity -> {
973-
Long relatedInternalId = idAndEntity.getT1().get();
966+
String relatedInternalId = idAndEntity.getT1().get();
974967
Entity savedEntity = idAndEntity.getT2().get();
975968
Neo4jPersistentProperty requiredIdProperty = targetEntity.getRequiredIdProperty();
976969
PersistentPropertyAccessor<?> targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject);
977-
Object actualRelatedId = targetPropertyAccessor.getProperty(requiredIdProperty);
978-
// if an internal id is used this must be set to link this entity in the next iteration
979-
if (targetEntity.isUsingInternalIds()) {
980-
if (relatedInternalId == null && actualRelatedId != null) {
981-
relatedInternalId = (Long) targetPropertyAccessor.getProperty(requiredIdProperty);
982-
} else if (actualRelatedId == null) {
983-
targetPropertyAccessor.setProperty(requiredIdProperty, relatedInternalId);
984-
}
985-
}
970+
Object possibleInternalLongId = targetPropertyAccessor.getProperty(requiredIdProperty);
971+
relatedInternalId = TemplateSupport.retrieveOrSetRelatedId(targetEntity, targetPropertyAccessor, Optional.ofNullable(savedEntity), relatedInternalId);
986972
if (savedEntity != null) {
987973
TemplateSupport.updateVersionPropertyIfPossible(targetEntity, targetPropertyAccessor, savedEntity);
988974
}
989-
stateMachine.markValueAsProcessedAs(relatedObjectBeforeCallbacksApplied, targetPropertyAccessor.getBean());
990-
stateMachine.markRelationshipAsProcessed(actualRelatedId == null ? relatedInternalId : actualRelatedId,
975+
stateMachine.markAsAliased(relatedObjectBeforeCallbacksApplied, targetPropertyAccessor.getBean());
976+
stateMachine.markRelationshipAsProcessed(possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId,
991977
relationshipDescription.getRelationshipObverse());
992978

993979
Object idValue = idProperty != null
@@ -1016,8 +1002,8 @@ private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity
10161002
.bind(idValue) //
10171003
.to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) //
10181004
.bindAll(statementHolder.getProperties())
1019-
.fetchAs(Long.class)
1020-
.mappedBy(IdentitySupport::getInternalId)
1005+
.fetchAs(Object.class)
1006+
.mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r))
10211007
.one()
10221008
.flatMap(relationshipInternalId -> {
10231009
if (idProperty != null && isNewRelationship) {

0 commit comments

Comments
 (0)