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
This commit is contained in:
Gerrit Meier
2023-08-14 23:21:10 +02:00
parent f94000e35e
commit d208634ec0
11 changed files with 233 additions and 38 deletions

View File

@@ -42,7 +42,9 @@ import org.apache.commons.logging.LogFactory;
import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.Condition;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.FunctionInvocation;
import org.neo4j.cypherdsl.core.Functions;
import org.neo4j.cypherdsl.core.Named;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.renderer.Configuration;
@@ -85,6 +87,7 @@ import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingS
import org.springframework.data.neo4j.core.mapping.NodeDescription;
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
import org.springframework.data.neo4j.core.mapping.SpringDataCypherDsl;
import org.springframework.data.neo4j.core.mapping.callback.EventSupport;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.neo4j.repository.NoResultException;
@@ -129,6 +132,8 @@ public final class Neo4jTemplate implements
private Renderer renderer;
private Function<Named, FunctionInvocation> elementIdOrIdFunction;
public Neo4jTemplate(Neo4jClient neo4jClient) {
this(neo4jClient, new Neo4jMappingContext());
}
@@ -148,6 +153,7 @@ public final class Neo4jTemplate implements
this.cypherGenerator = CypherGenerator.INSTANCE;
this.eventSupport = EventSupport.useExistingCallbacks(neo4jMappingContext, entityCallbacks);
this.renderer = Renderer.getDefaultRenderer();
this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction.apply(null);
}
ProjectionFactory getProjectionFactory() {
@@ -517,7 +523,7 @@ public final class Neo4jTemplate implements
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
.bind(entityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM)
.fetchAs(Map.Entry.class)
.mappedBy((t, r) -> new AbstractMap.SimpleEntry<>(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_ELEMENT_ID).asString()))
.mappedBy((t, r) -> new AbstractMap.SimpleEntry<>(r.get(Constants.NAME_OF_ID), TemplateSupport.convertIdOrElementIdToString(r.get(Constants.NAME_OF_ELEMENT_ID))))
.all()
.stream()
.collect(Collectors.toMap(m -> (Value) m.getKey(), m -> (String) m.getValue()));
@@ -746,7 +752,7 @@ public final class Neo4jTemplate implements
idProperty = null;
} else {
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity();
idProperty = relationshipPropertiesEntity.getIdProperty();
idProperty = relationshipPropertiesEntity.getIdProperty();
}
// break recursive procession and deletion of previously created relationships
@@ -1023,6 +1029,8 @@ public final class Neo4jTemplate implements
.getBeanProvider(Configuration.class)
.getIfAvailable(Configuration::defaultConfig);
this.renderer = Renderer.getRenderer(cypherDslConfiguration);
this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction.apply(cypherDslConfiguration.getDialect());
this.cypherGenerator.setElementIdOrIdFunction(elementIdOrIdFunction);
}
// only used for the CDI configuration
@@ -1178,7 +1186,7 @@ public final class Neo4jTemplate implements
.bindAll(usedParameters)
.fetchAs(Value.class).mappedBy((t, r) -> r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE))
.one()
.map(value -> value.asList(Value::asString))
.map(value -> value.asList(TemplateSupport::convertIdOrElementIdToString))
.get());
if (rootNodeIds.isEmpty()) {
@@ -1204,7 +1212,7 @@ public final class Neo4jTemplate implements
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription, PropertyPathWalkStep.empty()));
}
return new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipIds, relatedNodeIds, queryFragments);
return new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipIds, relatedNodeIds, queryFragments, elementIdOrIdFunction);
}
private void iterateNextLevel(Collection<String> nodeIds, RelationshipDescription sourceRelationshipDescription, Set<String> relationshipIds,
@@ -1235,11 +1243,11 @@ public final class Neo4jTemplate implements
Statement statement = cypherGenerator
.prepareMatchOf(target, relationshipDescription, null,
Functions.elementId(node).in(Cypher.parameter(Constants.NAME_OF_IDS)))
elementIdOrIdFunction.apply(node).in(Cypher.parameter(Constants.NAME_OF_IDS)))
.returning(cypherGenerator.createGenericReturnStatement()).build();
neo4jClient.query(renderer.render(statement))
.bindAll(Collections.singletonMap(Constants.NAME_OF_IDS, nodeIds))
.bindAll(Collections.singletonMap(Constants.NAME_OF_IDS, TemplateSupport.convertToLongIdOrStringElementId(nodeIds)))
.fetch()
.one()
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription, nextPathStep));
@@ -1254,11 +1262,11 @@ public final class Neo4jTemplate implements
return record -> {
@SuppressWarnings("unchecked")
List<String> newRelationshipIds = (List<String>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS);
List<String> newRelationshipIds = ((List<Object>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS)).stream().map(TemplateSupport::convertIdOrElementIdToString).toList();
relationshipIds.addAll(newRelationshipIds);
@SuppressWarnings("unchecked")
List<String> newRelatedNodeIds = (List<String>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES);
List<String> newRelatedNodeIds = ((List<Object>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)).stream().map(TemplateSupport::convertIdOrElementIdToString).toList();
Set<String> relatedIds = new HashSet<>(newRelatedNodeIds);
// use this list to get down the road

View File

@@ -19,8 +19,11 @@ import static org.neo4j.cypherdsl.core.Cypher.anyNode;
import static org.neo4j.cypherdsl.core.Cypher.asterisk;
import static org.neo4j.cypherdsl.core.Cypher.parameter;
import org.neo4j.cypherdsl.core.FunctionInvocation;
import org.neo4j.cypherdsl.core.Named;
import org.neo4j.driver.Values;
import org.springframework.data.neo4j.core.mapping.IdDescription;
import org.springframework.data.neo4j.core.mapping.SpringDataCypherDsl;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
@@ -133,6 +136,7 @@ public final class ReactiveNeo4jTemplate implements
private ProjectionFactory projectionFactory;
private Renderer renderer;
private Function<Named, FunctionInvocation> elementIdOrIdFunction;
public ReactiveNeo4jTemplate(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext) {
@@ -144,6 +148,7 @@ public final class ReactiveNeo4jTemplate implements
this.cypherGenerator = CypherGenerator.INSTANCE;
this.eventSupport = ReactiveEventSupport.useExistingCallbacks(neo4jMappingContext, ReactiveEntityCallbacks.create());
this.renderer = Renderer.getDefaultRenderer();
this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction.apply(null);
}
ProjectionFactory getProjectionFactory() {
@@ -586,7 +591,7 @@ public final class ReactiveNeo4jTemplate implements
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
.bind(boundedEntityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM)
.fetchAs(Tuple2.class)
.mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_ELEMENT_ID).asString()))
.mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), TemplateSupport.convertIdOrElementIdToString(r.get(Constants.NAME_OF_ELEMENT_ID))))
.all()
.collectMap(m -> (Value) m.getT1(), m -> (String) m.getT2());
}).flatMapMany(idToInternalIdMapping -> Flux.fromIterable(entitiesToBeSaved)
@@ -729,10 +734,10 @@ public final class ReactiveNeo4jTemplate implements
.bindAll(usedParameters)
.fetchAs(Tuple2.class)
.mappedBy((t, r) -> {
Collection<String> rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asList(Value::asString);
Collection<String> rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asList(TemplateSupport::convertIdOrElementIdToString);
rootNodeIds.addAll(rootIds);
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asString);
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asString);
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(TemplateSupport::convertIdOrElementIdToString);
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(TemplateSupport::convertIdOrElementIdToString);
return Tuples.of(newRelationshipIds, newRelatedNodeIds);
})
.one()
@@ -742,7 +747,7 @@ public final class ReactiveNeo4jTemplate implements
})
.expand(iterateAndMapNextLevel(relationshipDescription, queryFragments, rootClass, PropertyPathWalkStep.empty()));
})
.then(Mono.fromSupplier(() -> new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, processedRelationshipIds, processedNodeIds, queryFragments)));
.then(Mono.fromSupplier(() -> new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, processedRelationshipIds, processedNodeIds, queryFragments, elementIdOrIdFunction)));
})
.contextWrite(ctx -> ctx
.put("rootNodes", ConcurrentHashMap.newKeySet())
@@ -777,15 +782,15 @@ public final class ReactiveNeo4jTemplate implements
Statement statement = cypherGenerator
.prepareMatchOf(target, relDe, null,
Functions.elementId(node).in(Cypher.parameter(Constants.NAME_OF_ID)))
elementIdOrIdFunction.apply(node).in(Cypher.parameter(Constants.NAME_OF_ID)))
.returning(cypherGenerator.createGenericReturnStatement()).build();
return neo4jClient.query(renderer.render(statement))
.bindAll(Collections.singletonMap(Constants.NAME_OF_ID, relatedNodeIds))
.bindAll(Collections.singletonMap(Constants.NAME_OF_ID, TemplateSupport.convertToLongIdOrStringElementId(relatedNodeIds)))
.fetchAs(Tuple2.class)
.mappedBy((t, r) -> {
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asString);
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asString);
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(TemplateSupport::convertIdOrElementIdToString);
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(TemplateSupport::convertIdOrElementIdToString);
return Tuples.of(newRelationshipIds, newRelatedNodeIds);
})
@@ -1151,6 +1156,8 @@ public final class ReactiveNeo4jTemplate implements
.getBeanProvider(Configuration.class)
.getIfAvailable(Configuration::defaultConfig);
this.renderer = Renderer.getRenderer(cypherDslConfiguration);
this.elementIdOrIdFunction = SpringDataCypherDsl.elementIdOrIdFunction.apply(cypherDslConfiguration.getDialect());
this.cypherGenerator.setElementIdOrIdFunction(elementIdOrIdFunction);
}
@Override

View File

@@ -35,11 +35,15 @@ import java.util.stream.StreamSupport;
import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.FunctionInvocation;
import org.neo4j.cypherdsl.core.Functions;
import org.neo4j.cypherdsl.core.Named;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Relationship;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.renderer.Dialect;
import org.neo4j.cypherdsl.core.renderer.Renderer;
import org.neo4j.driver.Value;
import org.neo4j.driver.types.Entity;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.TypeSystem;
@@ -55,6 +59,7 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.core.mapping.NodeDescription;
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
import org.springframework.data.neo4j.core.mapping.PropertyTraverser;
import org.springframework.data.neo4j.core.mapping.SpringDataCypherDsl;
import org.springframework.data.neo4j.repository.query.QueryFragments;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -69,6 +74,7 @@ import org.springframework.util.Assert;
@API(status = API.Status.INTERNAL, since = "6.0.9")
public final class TemplateSupport {
/**
* Indicator for an empty collection
*/
@@ -181,13 +187,15 @@ public final class TemplateSupport {
private final static String RELATED_NODE_IDS = "relatedNodeIds";
final static NodesAndRelationshipsByIdStatementProvider EMPTY =
new NodesAndRelationshipsByIdStatementProvider(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), new QueryFragments());
new NodesAndRelationshipsByIdStatementProvider(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), new QueryFragments(), SpringDataCypherDsl.elementIdOrIdFunction.apply(Dialect.DEFAULT));
private final Map<String, Collection<String>> parameters = new HashMap<>(3);
private final QueryFragments queryFragments;
private final Function<Named, FunctionInvocation> elementIdFunction;
NodesAndRelationshipsByIdStatementProvider(Collection<String> rootNodeIds, Collection<String> relationshipsIds, Collection<String> relatedNodeIds, QueryFragments queryFragments) {
NodesAndRelationshipsByIdStatementProvider(Collection<String> rootNodeIds, Collection<String> relationshipsIds, Collection<String> relatedNodeIds, QueryFragments queryFragments, Function<Named, FunctionInvocation> elementIdFunction) {
this.elementIdFunction = elementIdFunction;
this.parameters.put(ROOT_NODE_IDS, rootNodeIds);
this.parameters.put(RELATIONSHIP_IDS, relationshipsIds);
this.parameters.put(RELATED_NODE_IDS, relatedNodeIds);
@@ -195,7 +203,12 @@ public final class TemplateSupport {
}
Map<String, Object> getParameters() {
return Collections.unmodifiableMap(parameters);
Map<String, Object> result = new HashMap<>(3);
result.put(ROOT_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(ROOT_NODE_IDS)));
result.put(RELATIONSHIP_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATIONSHIP_IDS)));
result.put(RELATED_NODE_IDS, convertToLongIdOrStringElementId(this.parameters.get(RELATED_NODE_IDS)));
return Collections.unmodifiableMap(result);
}
boolean hasRootNodeIds() {
@@ -209,13 +222,13 @@ public final class TemplateSupport {
Node relatedNodes = Cypher.anyNode(RELATED_NODE_IDS);
Relationship relationships = Cypher.anyNode().relationshipBetween(Cypher.anyNode()).named(RELATIONSHIP_IDS);
return Cypher.match(rootNodes)
.where(Functions.elementId(rootNodes).in(Cypher.parameter(ROOT_NODE_IDS)))
.where(elementIdFunction.apply(rootNodes).in(Cypher.parameter(ROOT_NODE_IDS)))
.with(Functions.collect(rootNodes).as(Constants.NAME_OF_ROOT_NODE))
.optionalMatch(relationships)
.where(Functions.elementId(relationships).in(Cypher.parameter(RELATIONSHIP_IDS)))
.where(elementIdFunction.apply(relationships).in(Cypher.parameter(RELATIONSHIP_IDS)))
.with(Constants.NAME_OF_ROOT_NODE, Functions.collectDistinct(relationships).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS))
.optionalMatch(relatedNodes)
.where(Functions.elementId(relatedNodes).in(Cypher.parameter(RELATED_NODE_IDS)))
.where(elementIdFunction.apply(relatedNodes).in(Cypher.parameter(RELATED_NODE_IDS)))
.with(
Constants.NAME_OF_ROOT_NODE,
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
@@ -414,10 +427,35 @@ public final class TemplateSupport {
* @return {@literal true} if renderer will use elementId
*/
static boolean rendererCanUseElementIdIfPresent(Renderer renderer, Neo4jPersistentEntity<?> targetEntity) {
return !targetEntity.isUsingDeprecatedInternalId() && targetEntity.isUsingInternalIds() && renderer.render(Cypher.returning(Functions.elementId(Cypher.anyNode("n"))).build())
return !targetEntity.isUsingDeprecatedInternalId() && targetEntity.isUsingInternalIds() && rendererRendersElementId(renderer);
}
private static boolean rendererRendersElementId(Renderer renderer) {
return renderer.render(Cypher.returning(Functions.elementId(Cypher.anyNode("n"))).build())
.equals("RETURN elementId(n)");
}
public static String convertIdOrElementIdToString(Object value) {
if (value instanceof Value driverValue) {
if (driverValue.hasType(TypeSystem.getDefault().NUMBER())) {
return driverValue.asNumber().toString();
}
return driverValue.asString();
}
return value.toString();
}
static Object convertToLongIdOrStringElementId(Collection<String> ids) {
try {
return ids.stream()
.map(Long::valueOf).collect(Collectors.toSet());
} catch (Exception e) {
return ids;
}
}
private TemplateSupport() {
}
}

View File

@@ -42,9 +42,11 @@ import org.neo4j.cypherdsl.core.Condition;
import org.neo4j.cypherdsl.core.Conditions;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Expression;
import org.neo4j.cypherdsl.core.FunctionInvocation;
import org.neo4j.cypherdsl.core.Functions;
import org.neo4j.cypherdsl.core.IdentifiableElement;
import org.neo4j.cypherdsl.core.MapProjection;
import org.neo4j.cypherdsl.core.Named;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Parameter;
import org.neo4j.cypherdsl.core.PatternElement;
@@ -82,6 +84,27 @@ public enum CypherGenerator {
INSTANCE;
// keeping elementId/id function selection in one place within this class
// default elementId function
private Function<Named, FunctionInvocation> elementIdOrIdFunction = named -> {
if (named instanceof Node node) {
return Functions.elementId(node);
} else if (named instanceof Relationship relationship) {
return Functions.elementId(relationship);
} else {
throw new IllegalArgumentException("Unsupported CypherDSL type: " + named.getClass());
}
};
/**
* Set function to be used to query either elementId or id.
*
* @param elementIdOrIdFunction new function to use.
*/
public void setElementIdOrIdFunction(Function<Named, FunctionInvocation> elementIdOrIdFunction) {
this.elementIdOrIdFunction = elementIdOrIdFunction;
}
private static final SymbolicName START_NODE_NAME = Cypher.name("startNode");
private static final SymbolicName END_NODE_NAME = Cypher.name("endNode");
@@ -120,7 +143,7 @@ public enum CypherGenerator {
if (nodeDescription instanceof Neo4jPersistentEntity<?> entity && entity.isUsingDeprecatedInternalId()) {
expressions.add(Functions.id(rootNode).as(Constants.NAME_OF_INTERNAL_ID));
}
expressions.add(Functions.elementId(rootNode).as(Constants.NAME_OF_ELEMENT_ID));
expressions.add(elementIdOrIdFunction.apply(rootNode).as(Constants.NAME_OF_ELEMENT_ID));
return match(rootNode).where(conditionOrNoCondition(condition)).with(expressions.toArray(IdentifiableElement[]::new));
}
@@ -133,7 +156,7 @@ public enum CypherGenerator {
StatementBuilder.OngoingReadingWithoutWhere match = prepareMatchOfRootNode(rootNode, initialMatchOn);
List<IdentifiableElement> expressions = new ArrayList<>();
expressions.add(Functions.collect(Functions.elementId(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
expressions.add(Functions.collect(elementIdOrIdFunction.apply(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
return match
.where(conditionOrNoCondition(condition))
@@ -170,9 +193,9 @@ public enum CypherGenerator {
relationship = relationship.named(Constants.NAME_OF_SYNTHESIZED_RELATIONS);
List<IdentifiableElement> expressions = new ArrayList<>();
expressions.add(Functions.collect(Functions.elementId(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
expressions.add(Functions.collect(Functions.elementId(targetNode)).as(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
expressions.add(Functions.collect(Functions.elementId(relationship)).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
expressions.add(Functions.collect(elementIdOrIdFunction.apply(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
expressions.add(Functions.collect(elementIdOrIdFunction.apply(targetNode)).as(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
expressions.add(Functions.collect(elementIdOrIdFunction.apply(relationship)).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
return match
.where(conditionOrNoCondition(condition))
@@ -400,7 +423,7 @@ public enum CypherGenerator {
if (nodeDescription instanceof Neo4jPersistentEntity<?> entity && entity.isUsingDeprecatedInternalId()) {
Functions.id(rootNode).as(Constants.NAME_OF_INTERNAL_ID);
}
expressions.add(Functions.elementId(rootNode).as(Constants.NAME_OF_ELEMENT_ID));
expressions.add(elementIdOrIdFunction.apply(rootNode).as(Constants.NAME_OF_ELEMENT_ID));
expressions.add(rootNode.property(nameOfIdProperty).as(Constants.NAME_OF_ID));
String row = "entity";
@@ -597,12 +620,12 @@ public enum CypherGenerator {
.mutate(RELATIONSHIP_NAME, relationshipProperties).build();
}
private static List<Expression> getReturnedIdExpressionsForRelationship(RelationshipDescription relationship, Relationship relationshipFragment) {
private List<Expression> getReturnedIdExpressionsForRelationship(RelationshipDescription relationship, Relationship relationshipFragment) {
List<Expression> result = new ArrayList<>();
if (relationship.hasRelationshipProperties() && relationship.getRelationshipPropertiesEntity() instanceof Neo4jPersistentEntity<?> entity && entity.isUsingDeprecatedInternalId()) {
result.add(Functions.id(relationshipFragment).as(Constants.NAME_OF_INTERNAL_ID));
}
result.add(Functions.elementId(relationshipFragment).as(Constants.NAME_OF_ELEMENT_ID));
result.add(elementIdOrIdFunction.apply(relationshipFragment).as(Constants.NAME_OF_ELEMENT_ID));
return result;
}
@@ -786,7 +809,7 @@ public enum CypherGenerator {
nodePropertiesProjection.add(Functions.id(node));
}
nodePropertiesProjection.add(Constants.NAME_OF_ELEMENT_ID);
nodePropertiesProjection.add(Functions.elementId(node));
nodePropertiesProjection.add(elementIdOrIdFunction.apply(node));
return nodePropertiesProjection;
}

View File

@@ -20,10 +20,12 @@ import static org.apiguardian.api.API.Status.INTERNAL;
import java.util.function.Function;
import org.apiguardian.api.API;
import org.neo4j.driver.Value;
import org.neo4j.driver.types.Entity;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.Node;
import org.neo4j.driver.types.Relationship;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
@@ -51,6 +53,7 @@ public final class IdentitySupport {
return entity.elementId();
}
/**
* Retrieves an identity either from attributes inside the row or if it is an actual entity, with the dedicated accessors.
*
@@ -64,11 +67,14 @@ public final class IdentitySupport {
}
var columnToUse = Constants.NAME_OF_ELEMENT_ID;
if (row.get(columnToUse) == null || row.get(columnToUse).isNull()) {
Value value = row.get(columnToUse);
if (value == null || value.isNull()) {
return null;
}
return row.get(columnToUse).asString();
if (value.hasType(TypeSystem.getDefault().NUMBER())) {
return value.asNumber().toString();
}
return value.asString();
}
@Nullable
@@ -93,7 +99,11 @@ public final class IdentitySupport {
} else if (queryResult instanceof Relationship) {
return "R" + seed + getElementId(queryResult);
} else if (!(queryResult.get(Constants.NAME_OF_ELEMENT_ID) == null || queryResult.get(Constants.NAME_OF_ELEMENT_ID).isNull())) {
return "N" + queryResult.get(Constants.NAME_OF_ELEMENT_ID).asString();
Value value = queryResult.get(Constants.NAME_OF_ELEMENT_ID);
if (value.hasType(TypeSystem.getDefault().NUMBER())) {
return "N" + value.asNumber();
}
return "N" + value.asString();
}
return null;

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2011-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.mapping;
import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.FunctionInvocation;
import org.neo4j.cypherdsl.core.Functions;
import org.neo4j.cypherdsl.core.Named;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Relationship;
import org.neo4j.cypherdsl.core.renderer.Dialect;
import java.util.function.Function;
/**
* Supporting class for CypherDSL related customizations.
*
* @author Gerrit Meier
*/
@API(status = API.Status.INTERNAL)
public final class SpringDataCypherDsl {
private SpringDataCypherDsl() {
}
public static Function<Dialect, Function<Named, FunctionInvocation>> elementIdOrIdFunction = dialect -> {
if (dialect == Dialect.NEO4J_5) {
return SpringDataCypherDsl::elementId;
} else if (dialect == Dialect.DEFAULT) {
return SpringDataCypherDsl::id;
} else {
return named -> {
if (named instanceof Node node) {
return Functions.elementId(node);
} else if (named instanceof Relationship relationship) {
return Functions.elementId(relationship);
} else {
throw new IllegalArgumentException("Unsupported CypherDSL type: " + named.getClass());
}
};
}
};
private static FunctionInvocation id(Named expression) {
return FunctionInvocation.create(new ElementIdOrIdFunctionDefinition("id"), expression.getRequiredSymbolicName());
}
private static FunctionInvocation elementId(Named expression) {
return FunctionInvocation.create(new ElementIdOrIdFunctionDefinition("elementId"), expression.getRequiredSymbolicName());
}
private static final class ElementIdOrIdFunctionDefinition implements FunctionInvocation.FunctionDefinition {
final String identifierFunction;
private ElementIdOrIdFunctionDefinition(String identifierFunction) {
this.identifierFunction = identifierFunction;
}
@Override
public String getImplementationName() {
return identifierFunction;
}
@Override
public boolean isAggregate() {
return false;
}
}
}

View File

@@ -30,6 +30,8 @@ import jakarta.inject.Singleton;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.neo4j.cypherdsl.core.renderer.Configuration;
import org.neo4j.cypherdsl.core.renderer.Dialect;
import org.neo4j.driver.Driver;
import org.springframework.data.neo4j.config.Neo4jCdiExtension;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
@@ -55,6 +57,16 @@ class Neo4jCdiExtensionIT {
public Driver driver() {
return connectionSupport.getDriver();
}
@Produces
@Singleton
public Configuration cypherDslConfiguration() {
if (connectionSupport.isCypher5SyntaxCompatible()) {
return Configuration.newConfig().withDialect(Dialect.NEO4J_5).build();
}
return Configuration.newConfig().withDialect(Dialect.DEFAULT).build();
}
}
@ApplicationScoped

View File

@@ -20,12 +20,14 @@ import java.util.function.Predicate;
import java.util.regex.Pattern;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.neo4j.test.Neo4jExtension;
@Tag(Neo4jExtension.NEEDS_VERSION_SUPPORTING_ELEMENT_ID)
abstract class AbstractElementIdTestBase {
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;

View File

@@ -61,6 +61,7 @@ import static org.assertj.core.api.Assumptions.assumeThat;
public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback {
public final static String NEEDS_REACTIVE_SUPPORT = "reactive-test";
public final static String NEEDS_VERSION_SUPPORTING_ELEMENT_ID = "elementid-test";
public final static String COMMUNITY_EDITION_ONLY = "community-edition";
public final static String COMMERCIAL_EDITION_ONLY = "commercial-edition";
/**
@@ -136,6 +137,10 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback {
assumeThat(neo4jConnectionSupport.getServerVersion().greaterThanOrEqual(ServerVersion.v4_0_0))
.describedAs("This test requires at least Neo4j 4.0 for reactive database connectivity.").isTrue();
}
if (tags.contains(NEEDS_VERSION_SUPPORTING_ELEMENT_ID)) {
assumeThat(neo4jConnectionSupport.getServerVersion().greaterThan(ServerVersion.v5_3_0))
.describedAs("This test requires a version greater than Neo4j 5.3.0 for correct elementId handling.").isTrue();
}
if (tags.contains(COMMUNITY_EDITION_ONLY)) {
assumeThat(neo4jConnectionSupport.isCommunityEdition())

View File

@@ -27,6 +27,7 @@ import java.util.regex.Pattern;
public final class ServerVersion {
public static final String NEO4J_PRODUCT = "Neo4j";
public static final ServerVersion v5_3_0 = new ServerVersion(NEO4J_PRODUCT, 5, 3, 0);
public static final ServerVersion v5_0_0 = new ServerVersion(NEO4J_PRODUCT, 5, 0, 0);
public static final ServerVersion v4_4_0 = new ServerVersion(NEO4J_PRODUCT, 4, 4, 0);
public static final ServerVersion v4_3_0 = new ServerVersion(NEO4J_PRODUCT, 4, 3, 0);

View File

@@ -32,6 +32,7 @@ import org.springframework.data.neo4j.repository.Neo4jRepository
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories
import org.springframework.data.neo4j.test.BookmarkCapture
import org.springframework.data.neo4j.test.Neo4jExtension
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration
import org.springframework.data.neo4j.test.Neo4jIntegrationTest
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.annotation.EnableTransactionManagement
@@ -119,7 +120,7 @@ class ImmutableRelationshipsIT @Autowired constructor(
@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories
open class MyConfig : AbstractNeo4jConfig() {
open class MyConfig : Neo4jImperativeTestConfiguration() {
@Bean
override fun driver(): Driver {
return neo4jConnectionSupport.driver
@@ -135,6 +136,10 @@ class ImmutableRelationshipsIT @Autowired constructor(
val bookmarkCapture = bookmarkCapture()
return Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture))
}
override fun isCypher5Compatible(): Boolean {
return neo4jConnectionSupport.isCypher5SyntaxCompatible
}
}
}