Skip to content

Commit 5148521

Browse files
committed
GH-2784 - Switch between elementId and id in Cypher usage.
Works now with Neo4j 4.4 and 5.x. Instead of having a unified return type (String) in the Cypher result, SDN accepts now `Long` for id() and `String`. The conversion will take place in the library code. To switch between id() and elementId(), SDN will inspect the selected CypherDSL dialect and select the needed identifier function accordingly. Closes #2784
1 parent 620392c commit 5148521

File tree

11 files changed

+233
-38
lines changed

11 files changed

+233
-38
lines changed

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
import org.apiguardian.api.API;
4343
import org.neo4j.cypherdsl.core.Condition;
4444
import org.neo4j.cypherdsl.core.Cypher;
45+
import org.neo4j.cypherdsl.core.FunctionInvocation;
4546
import org.neo4j.cypherdsl.core.Functions;
47+
import org.neo4j.cypherdsl.core.Named;
4648
import org.neo4j.cypherdsl.core.Node;
4749
import org.neo4j.cypherdsl.core.Statement;
4850
import org.neo4j.cypherdsl.core.renderer.Configuration;
@@ -85,6 +87,7 @@
8587
import org.springframework.data.neo4j.core.mapping.NodeDescription;
8688
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
8789
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
90+
import org.springframework.data.neo4j.core.mapping.SpringDataCypherDsl;
8891
import org.springframework.data.neo4j.core.mapping.callback.EventSupport;
8992
import org.springframework.data.neo4j.core.schema.TargetNode;
9093
import org.springframework.data.neo4j.repository.NoResultException;
@@ -129,6 +132,8 @@ public final class Neo4jTemplate implements
129132

130133
private Renderer renderer;
131134

135+
private Function<Named, FunctionInvocation> elementIdOrIdFunction;
136+
132137
public Neo4jTemplate(Neo4jClient neo4jClient) {
133138
this(neo4jClient, new Neo4jMappingContext());
134139
}
@@ -148,6 +153,7 @@ public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingCo
148153
this.cypherGenerator = CypherGenerator.INSTANCE;
149154
this.eventSupport = EventSupport.useExistingCallbacks(neo4jMappingContext, entityCallbacks);
150155
this.renderer = Renderer.getDefaultRenderer();
156+
this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction.apply(null);
151157
}
152158

153159
ProjectionFactory getProjectionFactory() {
@@ -517,7 +523,7 @@ class Tuple3<T> {
517523
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
518524
.bind(entityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM)
519525
.fetchAs(Map.Entry.class)
520-
.mappedBy((t, r) -> new AbstractMap.SimpleEntry<>(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_ELEMENT_ID).asString()))
526+
.mappedBy((t, r) -> new AbstractMap.SimpleEntry<>(r.get(Constants.NAME_OF_ID), TemplateSupport.convertIdOrElementIdToString(r.get(Constants.NAME_OF_ELEMENT_ID))))
521527
.all()
522528
.stream()
523529
.collect(Collectors.toMap(m -> (Value) m.getKey(), m -> (String) m.getValue()));
@@ -746,7 +752,7 @@ private <T> T processNestedRelations(
746752
idProperty = null;
747753
} else {
748754
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity();
749-
idProperty = relationshipPropertiesEntity.getIdProperty();
755+
idProperty = relationshipPropertiesEntity.getIdProperty();
750756
}
751757

752758
// break recursive procession and deletion of previously created relationships
@@ -1023,6 +1029,8 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
10231029
.getBeanProvider(Configuration.class)
10241030
.getIfAvailable(Configuration::defaultConfig);
10251031
this.renderer = Renderer.getRenderer(cypherDslConfiguration);
1032+
this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction.apply(cypherDslConfiguration.getDialect());
1033+
this.cypherGenerator.setElementIdOrIdFunction(elementIdOrIdFunction);
10261034
}
10271035

10281036
// only used for the CDI configuration
@@ -1178,7 +1186,7 @@ private NodesAndRelationshipsByIdStatementProvider createNodesAndRelationshipsBy
11781186
.bindAll(usedParameters)
11791187
.fetchAs(Value.class).mappedBy((t, r) -> r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE))
11801188
.one()
1181-
.map(value -> value.asList(Value::asString))
1189+
.map(value -> value.asList(TemplateSupport::convertIdOrElementIdToString))
11821190
.get());
11831191

11841192
if (rootNodeIds.isEmpty()) {
@@ -1204,7 +1212,7 @@ private NodesAndRelationshipsByIdStatementProvider createNodesAndRelationshipsBy
12041212
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription, PropertyPathWalkStep.empty()));
12051213
}
12061214

1207-
return new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipIds, relatedNodeIds, queryFragments);
1215+
return new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipIds, relatedNodeIds, queryFragments, elementIdOrIdFunction);
12081216
}
12091217

12101218
private void iterateNextLevel(Collection<String> nodeIds, RelationshipDescription sourceRelationshipDescription, Set<String> relationshipIds,
@@ -1235,11 +1243,11 @@ private void iterateNextLevel(Collection<String> nodeIds, RelationshipDescriptio
12351243

12361244
Statement statement = cypherGenerator
12371245
.prepareMatchOf(target, relationshipDescription, null,
1238-
Functions.elementId(node).in(Cypher.parameter(Constants.NAME_OF_IDS)))
1246+
elementIdOrIdFunction.apply(node).in(Cypher.parameter(Constants.NAME_OF_IDS)))
12391247
.returning(cypherGenerator.createGenericReturnStatement()).build();
12401248

12411249
neo4jClient.query(renderer.render(statement))
1242-
.bindAll(Collections.singletonMap(Constants.NAME_OF_IDS, nodeIds))
1250+
.bindAll(Collections.singletonMap(Constants.NAME_OF_IDS, TemplateSupport.convertToLongIdOrStringElementId(nodeIds)))
12431251
.fetch()
12441252
.one()
12451253
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription, nextPathStep));
@@ -1254,11 +1262,11 @@ private Consumer<Map<String, Object>> iterateAndMapNextLevel(Set<String> relatio
12541262

12551263
return record -> {
12561264
@SuppressWarnings("unchecked")
1257-
List<String> newRelationshipIds = (List<String>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS);
1265+
List<String> newRelationshipIds = ((List<Object>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS)).stream().map(TemplateSupport::convertIdOrElementIdToString).toList();
12581266
relationshipIds.addAll(newRelationshipIds);
12591267

12601268
@SuppressWarnings("unchecked")
1261-
List<String> newRelatedNodeIds = (List<String>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES);
1269+
List<String> newRelatedNodeIds = ((List<Object>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)).stream().map(TemplateSupport::convertIdOrElementIdToString).toList();
12621270

12631271
Set<String> relatedIds = new HashSet<>(newRelatedNodeIds);
12641272
// use this list to get down the road

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@
1919
import static org.neo4j.cypherdsl.core.Cypher.asterisk;
2020
import static org.neo4j.cypherdsl.core.Cypher.parameter;
2121

22+
import org.neo4j.cypherdsl.core.FunctionInvocation;
23+
import org.neo4j.cypherdsl.core.Named;
2224
import org.neo4j.driver.Values;
2325
import org.springframework.data.neo4j.core.mapping.IdDescription;
26+
import org.springframework.data.neo4j.core.mapping.SpringDataCypherDsl;
2427
import reactor.core.publisher.Flux;
2528
import reactor.core.publisher.Mono;
2629
import reactor.util.function.Tuple2;
@@ -133,6 +136,7 @@ public final class ReactiveNeo4jTemplate implements
133136
private ProjectionFactory projectionFactory;
134137

135138
private Renderer renderer;
139+
private Function<Named, FunctionInvocation> elementIdOrIdFunction;
136140

137141
public ReactiveNeo4jTemplate(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext) {
138142

@@ -144,6 +148,7 @@ public ReactiveNeo4jTemplate(ReactiveNeo4jClient neo4jClient, Neo4jMappingContex
144148
this.cypherGenerator = CypherGenerator.INSTANCE;
145149
this.eventSupport = ReactiveEventSupport.useExistingCallbacks(neo4jMappingContext, ReactiveEntityCallbacks.create());
146150
this.renderer = Renderer.getDefaultRenderer();
151+
this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction.apply(null);
147152
}
148153

149154
ProjectionFactory getProjectionFactory() {
@@ -586,7 +591,7 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Collection<Prop
586591
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
587592
.bind(boundedEntityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM)
588593
.fetchAs(Tuple2.class)
589-
.mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_ELEMENT_ID).asString()))
594+
.mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), TemplateSupport.convertIdOrElementIdToString(r.get(Constants.NAME_OF_ELEMENT_ID))))
590595
.all()
591596
.collectMap(m -> (Value) m.getT1(), m -> (String) m.getT2());
592597
}).flatMapMany(idToInternalIdMapping -> Flux.fromIterable(entitiesToBeSaved)
@@ -729,10 +734,10 @@ private Mono<NodesAndRelationshipsByIdStatementProvider> createNodesAndRelations
729734
.bindAll(usedParameters)
730735
.fetchAs(Tuple2.class)
731736
.mappedBy((t, r) -> {
732-
Collection<String> rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asList(Value::asString);
737+
Collection<String> rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asList(TemplateSupport::convertIdOrElementIdToString);
733738
rootNodeIds.addAll(rootIds);
734-
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asString);
735-
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asString);
739+
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(TemplateSupport::convertIdOrElementIdToString);
740+
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(TemplateSupport::convertIdOrElementIdToString);
736741
return Tuples.of(newRelationshipIds, newRelatedNodeIds);
737742
})
738743
.one()
@@ -742,7 +747,7 @@ private Mono<NodesAndRelationshipsByIdStatementProvider> createNodesAndRelations
742747
})
743748
.expand(iterateAndMapNextLevel(relationshipDescription, queryFragments, rootClass, PropertyPathWalkStep.empty()));
744749
})
745-
.then(Mono.fromSupplier(() -> new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, processedRelationshipIds, processedNodeIds, queryFragments)));
750+
.then(Mono.fromSupplier(() -> new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, processedRelationshipIds, processedNodeIds, queryFragments, elementIdOrIdFunction)));
746751
})
747752
.contextWrite(ctx -> ctx
748753
.put("rootNodes", ConcurrentHashMap.newKeySet())
@@ -777,15 +782,15 @@ private Flux<Tuple2<Collection<String>, Collection<String>>> iterateNextLevel(Co
777782

778783
Statement statement = cypherGenerator
779784
.prepareMatchOf(target, relDe, null,
780-
Functions.elementId(node).in(Cypher.parameter(Constants.NAME_OF_ID)))
785+
elementIdOrIdFunction.apply(node).in(Cypher.parameter(Constants.NAME_OF_ID)))
781786
.returning(cypherGenerator.createGenericReturnStatement()).build();
782787

783788
return neo4jClient.query(renderer.render(statement))
784-
.bindAll(Collections.singletonMap(Constants.NAME_OF_ID, relatedNodeIds))
789+
.bindAll(Collections.singletonMap(Constants.NAME_OF_ID, TemplateSupport.convertToLongIdOrStringElementId(relatedNodeIds)))
785790
.fetchAs(Tuple2.class)
786791
.mappedBy((t, r) -> {
787-
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asString);
788-
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asString);
792+
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(TemplateSupport::convertIdOrElementIdToString);
793+
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(TemplateSupport::convertIdOrElementIdToString);
789794

790795
return Tuples.of(newRelationshipIds, newRelatedNodeIds);
791796
})
@@ -1151,6 +1156,8 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
11511156
.getBeanProvider(Configuration.class)
11521157
.getIfAvailable(Configuration::defaultConfig);
11531158
this.renderer = Renderer.getRenderer(cypherDslConfiguration);
1159+
this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction.apply(cypherDslConfiguration.getDialect());
1160+
this.cypherGenerator.setElementIdOrIdFunction(elementIdOrIdFunction);
11541161
}
11551162

11561163
@Override

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

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,15 @@
3535

3636
import org.apiguardian.api.API;
3737
import org.neo4j.cypherdsl.core.Cypher;
38+
import org.neo4j.cypherdsl.core.FunctionInvocation;
3839
import org.neo4j.cypherdsl.core.Functions;
40+
import org.neo4j.cypherdsl.core.Named;
3941
import org.neo4j.cypherdsl.core.Node;
4042
import org.neo4j.cypherdsl.core.Relationship;
4143
import org.neo4j.cypherdsl.core.Statement;
44+
import org.neo4j.cypherdsl.core.renderer.Dialect;
4245
import org.neo4j.cypherdsl.core.renderer.Renderer;
46+
import org.neo4j.driver.Value;
4347
import org.neo4j.driver.types.Entity;
4448
import org.neo4j.driver.types.MapAccessor;
4549
import org.neo4j.driver.types.TypeSystem;
@@ -55,6 +59,7 @@
5559
import org.springframework.data.neo4j.core.mapping.NodeDescription;
5660
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
5761
import org.springframework.data.neo4j.core.mapping.PropertyTraverser;
62+
import org.springframework.data.neo4j.core.mapping.SpringDataCypherDsl;
5863
import org.springframework.data.neo4j.repository.query.QueryFragments;
5964
import org.springframework.lang.Nullable;
6065
import org.springframework.util.Assert;
@@ -69,6 +74,7 @@
6974
@API(status = API.Status.INTERNAL, since = "6.0.9")
7075
public final class TemplateSupport {
7176

77+
7278
/**
7379
* Indicator for an empty collection
7480
*/
@@ -181,21 +187,28 @@ static final class NodesAndRelationshipsByIdStatementProvider {
181187
private final static String RELATED_NODE_IDS = "relatedNodeIds";
182188

183189
final static NodesAndRelationshipsByIdStatementProvider EMPTY =
184-
new NodesAndRelationshipsByIdStatementProvider(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), new QueryFragments());
190+
new NodesAndRelationshipsByIdStatementProvider(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), new QueryFragments(), SpringDataCypherDsl.elementIdOrIdFunction.apply(Dialect.DEFAULT));
185191

186192
private final Map<String, Collection<String>> parameters = new HashMap<>(3);
187193
private final QueryFragments queryFragments;
194+
private final Function<Named, FunctionInvocation> elementIdFunction;
188195

189-
NodesAndRelationshipsByIdStatementProvider(Collection<String> rootNodeIds, Collection<String> relationshipsIds, Collection<String> relatedNodeIds, QueryFragments queryFragments) {
196+
NodesAndRelationshipsByIdStatementProvider(Collection<String> rootNodeIds, Collection<String> relationshipsIds, Collection<String> relatedNodeIds, QueryFragments queryFragments, Function<Named, FunctionInvocation> elementIdFunction) {
190197

198+
this.elementIdFunction = elementIdFunction;
191199
this.parameters.put(ROOT_NODE_IDS, rootNodeIds);
192200
this.parameters.put(RELATIONSHIP_IDS, relationshipsIds);
193201
this.parameters.put(RELATED_NODE_IDS, relatedNodeIds);
194202
this.queryFragments = queryFragments;
195203
}
196204

197205
Map<String, Object> getParameters() {
198-
return Collections.unmodifiableMap(parameters);
206+
Map<String, Object> result = new HashMap<>(3);
207+
result.put(ROOT_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(ROOT_NODE_IDS)));
208+
result.put(RELATIONSHIP_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATIONSHIP_IDS)));
209+
result.put(RELATED_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATED_NODE_IDS)));
210+
211+
return Collections.unmodifiableMap(result);
199212
}
200213

201214
boolean hasRootNodeIds() {
@@ -209,13 +222,13 @@ Statement toStatement(NodeDescription<?> nodeDescription) {
209222
Node relatedNodes = Cypher.anyNode(RELATED_NODE_IDS);
210223
Relationship relationships = Cypher.anyNode().relationshipBetween(Cypher.anyNode()).named(RELATIONSHIP_IDS);
211224
return Cypher.match(rootNodes)
212-
.where(Functions.elementId(rootNodes).in(Cypher.parameter(ROOT_NODE_IDS)))
225+
.where(elementIdFunction.apply(rootNodes).in(Cypher.parameter(ROOT_NODE_IDS)))
213226
.with(Functions.collect(rootNodes).as(Constants.NAME_OF_ROOT_NODE))
214227
.optionalMatch(relationships)
215-
.where(Functions.elementId(relationships).in(Cypher.parameter(RELATIONSHIP_IDS)))
228+
.where(elementIdFunction.apply(relationships).in(Cypher.parameter(RELATIONSHIP_IDS)))
216229
.with(Constants.NAME_OF_ROOT_NODE, Functions.collectDistinct(relationships).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS))
217230
.optionalMatch(relatedNodes)
218-
.where(Functions.elementId(relatedNodes).in(Cypher.parameter(RELATED_NODE_IDS)))
231+
.where(elementIdFunction.apply(relatedNodes).in(Cypher.parameter(RELATED_NODE_IDS)))
219232
.with(
220233
Constants.NAME_OF_ROOT_NODE,
221234
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
@@ -414,10 +427,35 @@ static <T> Object retrieveOrSetRelatedId(
414427
* @return {@literal true} if renderer will use elementId
415428
*/
416429
static boolean rendererCanUseElementIdIfPresent(Renderer renderer, Neo4jPersistentEntity<?> targetEntity) {
417-
return !targetEntity.isUsingDeprecatedInternalId() && targetEntity.isUsingInternalIds() && renderer.render(Cypher.returning(Functions.elementId(Cypher.anyNode("n"))).build())
430+
return !targetEntity.isUsingDeprecatedInternalId() && targetEntity.isUsingInternalIds() && rendererRendersElementId(renderer);
431+
}
432+
433+
private static boolean rendererRendersElementId(Renderer renderer) {
434+
return renderer.render(Cypher.returning(Functions.elementId(Cypher.anyNode("n"))).build())
418435
.equals("RETURN elementId(n)");
419436
}
420437

438+
public static String convertIdOrElementIdToString(Object value) {
439+
if (value instanceof Value driverValue) {
440+
if (driverValue.hasType(TypeSystem.getDefault().NUMBER())) {
441+
return driverValue.asNumber().toString();
442+
}
443+
return driverValue.asString();
444+
}
445+
446+
return value.toString();
447+
}
448+
449+
static Object convertToLongIdOrStringElementId(Collection<String> ids) {
450+
try {
451+
return ids.stream()
452+
.map(Long::valueOf).collect(Collectors.toSet());
453+
454+
} catch (Exception e) {
455+
return ids;
456+
}
457+
}
458+
421459
private TemplateSupport() {
422460
}
423461
}

0 commit comments

Comments
 (0)