diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 193fa6e25..7914e4aa2 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -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 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 nodeIds, RelationshipDescription sourceRelationshipDescription, Set 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 newRelationshipIds = (List) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS); + List newRelationshipIds = ((List) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS)).stream().map(TemplateSupport::convertIdOrElementIdToString).toList(); relationshipIds.addAll(newRelationshipIds); @SuppressWarnings("unchecked") - List newRelatedNodeIds = (List) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES); + List newRelatedNodeIds = ((List) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)).stream().map(TemplateSupport::convertIdOrElementIdToString).toList(); Set relatedIds = new HashSet<>(newRelatedNodeIds); // use this list to get down the road diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java index 53820c9ff..99f2cb661 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java @@ -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 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 rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asList(Value::asString); + Collection rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asList(TemplateSupport::convertIdOrElementIdToString); rootNodeIds.addAll(rootIds); - Collection newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asString); - Collection newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asString); + Collection newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(TemplateSupport::convertIdOrElementIdToString); + Collection 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 newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asString); - Collection newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asString); + Collection newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(TemplateSupport::convertIdOrElementIdToString); + Collection 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 diff --git a/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java b/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java index dbc3550a5..776bdac19 100644 --- a/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java @@ -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> parameters = new HashMap<>(3); private final QueryFragments queryFragments; + private final Function elementIdFunction; - NodesAndRelationshipsByIdStatementProvider(Collection rootNodeIds, Collection relationshipsIds, Collection relatedNodeIds, QueryFragments queryFragments) { + NodesAndRelationshipsByIdStatementProvider(Collection rootNodeIds, Collection relationshipsIds, Collection relatedNodeIds, QueryFragments queryFragments, Function 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 getParameters() { - return Collections.unmodifiableMap(parameters); + Map 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 ids) { + try { + return ids.stream() + .map(Long::valueOf).collect(Collectors.toSet()); + + } catch (Exception e) { + return ids; + } + } + private TemplateSupport() { } } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java index 9bdb49943..d150f5137 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java @@ -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 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 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 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 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 getReturnedIdExpressionsForRelationship(RelationshipDescription relationship, Relationship relationshipFragment) { + private List getReturnedIdExpressionsForRelationship(RelationshipDescription relationship, Relationship relationshipFragment) { List 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; } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java b/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java index ee8650a4b..661f7562a 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/IdentitySupport.java @@ -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; diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/SpringDataCypherDsl.java b/src/main/java/org/springframework/data/neo4j/core/mapping/SpringDataCypherDsl.java new file mode 100644 index 000000000..2bb38a2c9 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/SpringDataCypherDsl.java @@ -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> 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; + } + + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java b/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java index 0d002510e..488dbcf4f 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cdi/Neo4jCdiExtensionIT.java @@ -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 diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/AbstractElementIdTestBase.java b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/AbstractElementIdTestBase.java index eca189879..fd3f372ea 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/AbstractElementIdTestBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/pure_element_id/AbstractElementIdTestBase.java @@ -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; diff --git a/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java b/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java index e1ebfadad..024401e54 100644 --- a/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java +++ b/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java @@ -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()) diff --git a/src/test/java/org/springframework/data/neo4j/test/ServerVersion.java b/src/test/java/org/springframework/data/neo4j/test/ServerVersion.java index 9352cb626..719bf3b47 100644 --- a/src/test/java/org/springframework/data/neo4j/test/ServerVersion.java +++ b/src/test/java/org/springframework/data/neo4j/test/ServerVersion.java @@ -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); diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/ImmutableRelationshipsIT.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/ImmutableRelationshipsIT.kt index 5a515d401..98cc046b4 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/ImmutableRelationshipsIT.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/ImmutableRelationshipsIT.kt @@ -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 + } } }