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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user