GH-1907 - Clean-up relationships querying code.
See #1907. Not an implementation or a fix, but improvement of overall readability.
This commit is contained in:
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright 2011-2021 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;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
||||
final class GenericQueryAndParameters {
|
||||
|
||||
private final static String ROOT_NODE_IDS = "rootNodeIds";
|
||||
private final static String RELATIONSHIP_IDS = "relationshipIds";
|
||||
private final static String RELATED_NODE_IDS = "relatedNodeIds";
|
||||
|
||||
final static GenericQueryAndParameters EMPTY =
|
||||
new GenericQueryAndParameters(Collections.emptySet(), Collections.emptySet(), Collections.emptySet());
|
||||
|
||||
private final Map<String, Collection<Long>> parameters = new HashMap<>(3);
|
||||
|
||||
GenericQueryAndParameters(Collection<Long> rootNodeIds, Collection<Long> relationshipsIds, Collection<Long> relatedNodeIds) {
|
||||
parameters.put(ROOT_NODE_IDS, rootNodeIds);
|
||||
parameters.put(RELATIONSHIP_IDS, relationshipsIds);
|
||||
parameters.put(RELATED_NODE_IDS, relatedNodeIds);
|
||||
}
|
||||
|
||||
GenericQueryAndParameters() {
|
||||
this(new HashSet<>(), new HashSet<>(), new HashSet<>());
|
||||
}
|
||||
|
||||
void with(Collection<Long> rootNodeIds, Collection<Long> relationshipsIds, Collection<Long> relatedNodeIds) {
|
||||
parameters.put(ROOT_NODE_IDS, rootNodeIds);
|
||||
parameters.put(RELATIONSHIP_IDS, relationshipsIds);
|
||||
parameters.put(RELATED_NODE_IDS, relatedNodeIds);
|
||||
}
|
||||
|
||||
Map<String, Object> getParameters() {
|
||||
return Collections.unmodifiableMap(parameters);
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return parameters.get(ROOT_NODE_IDS).isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -56,6 +56,7 @@ import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.mapping.AssociationHandler;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.callback.EntityCallbacks;
|
||||
import org.springframework.data.neo4j.core.TemplateSupport.NodesAndRelationshipsByIdStatementProvider;
|
||||
import org.springframework.data.neo4j.core.mapping.Constants;
|
||||
import org.springframework.data.neo4j.core.mapping.CreateRelationshipStatementHolder;
|
||||
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
|
||||
@@ -70,6 +71,7 @@ import org.springframework.data.neo4j.core.mapping.NodeDescription;
|
||||
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
|
||||
import org.springframework.data.neo4j.core.mapping.callback.EventSupport;
|
||||
import org.springframework.data.neo4j.repository.NoResultException;
|
||||
import org.springframework.data.neo4j.repository.query.QueryFragments;
|
||||
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.ProjectionInformation;
|
||||
@@ -362,7 +364,7 @@ public final class Neo4jTemplate implements Neo4jOperations, FluentNeo4jOperatio
|
||||
private <T> DynamicLabels determineDynamicLabels(T entityToBeSaved, Neo4jPersistentEntity<?> entityMetaData) {
|
||||
return entityMetaData.getDynamicLabelsProperty().map(p -> {
|
||||
|
||||
PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
|
||||
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
|
||||
Neo4jClient.RunnableSpecTightToDatabase runnableQuery = neo4jClient
|
||||
.query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData)))
|
||||
.bind(propertyAccessor.getProperty(entityMetaData.getRequiredIdProperty()))
|
||||
@@ -410,14 +412,14 @@ public final class Neo4jTemplate implements Neo4jOperations, FluentNeo4jOperatio
|
||||
}
|
||||
|
||||
class Tuple3<T> {
|
||||
T t1;
|
||||
boolean t2;
|
||||
T t3;
|
||||
final T originalInstance;
|
||||
final boolean wasNew;
|
||||
final T modifiedInstance;
|
||||
|
||||
Tuple3(T t1, boolean t2, T t3) {
|
||||
this.t1 = t1;
|
||||
this.t2 = t2;
|
||||
this.t3 = t3;
|
||||
Tuple3(T originalInstance, boolean wasNew, T modifiedInstance) {
|
||||
this.originalInstance = originalInstance;
|
||||
this.wasNew = wasNew;
|
||||
this.modifiedInstance = modifiedInstance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +429,7 @@ public final class Neo4jTemplate implements Neo4jOperations, FluentNeo4jOperatio
|
||||
|
||||
// Save roots
|
||||
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass);
|
||||
List<Map<String, Object>> entityList = entitiesToBeSaved.stream().map(h -> h.t3).map(binderFunction)
|
||||
List<Map<String, Object>> entityList = entitiesToBeSaved.stream().map(h -> h.modifiedInstance).map(binderFunction)
|
||||
.collect(Collectors.toList());
|
||||
ResultSummary resultSummary = neo4jClient
|
||||
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
|
||||
@@ -441,8 +443,8 @@ public final class Neo4jTemplate implements Neo4jOperations, FluentNeo4jOperatio
|
||||
|
||||
// Save related
|
||||
return entitiesToBeSaved.stream().map(t -> {
|
||||
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(t.t3);
|
||||
return processRelations(entityMetaData, t.t1, propertyAccessor, t.t2, TemplateSupport.computeIncludePropertyPredicate(includedProperties));
|
||||
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(t.modifiedInstance);
|
||||
return processRelations(entityMetaData, t.originalInstance, propertyAccessor, t.wasNew, TemplateSupport.computeIncludePropertyPredicate(includedProperties));
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -848,21 +850,21 @@ public final class Neo4jTemplate implements Neo4jOperations, FluentNeo4jOperatio
|
||||
String cypherQuery = queryFragmentsAndParameters.getCypherQuery();
|
||||
Map<String, Object> finalParameters = queryFragmentsAndParameters.getParameters();
|
||||
|
||||
QueryFragmentsAndParameters.QueryFragments queryFragments = queryFragmentsAndParameters.getQueryFragments();
|
||||
QueryFragments queryFragments = queryFragmentsAndParameters.getQueryFragments();
|
||||
Neo4jPersistentEntity<?> entityMetaData = (Neo4jPersistentEntity<?>) queryFragmentsAndParameters.getNodeDescription();
|
||||
|
||||
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
|
||||
if (cypherQuery == null || containsPossibleCircles) {
|
||||
|
||||
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
|
||||
GenericQueryAndParameters genericQueryAndParameters =
|
||||
createQueryAndParameters(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters());
|
||||
NodesAndRelationshipsByIdStatementProvider nodesAndRelationshipsById =
|
||||
createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters());
|
||||
|
||||
if (genericQueryAndParameters.isEmpty()) {
|
||||
if (nodesAndRelationshipsById.hasRootNodeIds()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
cypherQuery = renderer.render(queryFragments.generateGenericStatement());
|
||||
finalParameters = genericQueryAndParameters.getParameters();
|
||||
cypherQuery = renderer.render(nodesAndRelationshipsById.toStatement());
|
||||
finalParameters = nodesAndRelationshipsById.getParameters();
|
||||
} else {
|
||||
Statement statement = queryFragments.toStatement();
|
||||
cypherQuery = renderer.render(statement);
|
||||
@@ -876,8 +878,8 @@ public final class Neo4jTemplate implements Neo4jOperations, FluentNeo4jOperatio
|
||||
.map(f -> newMappingSpec.mappedBy(f)).orElse(newMappingSpec));
|
||||
}
|
||||
|
||||
private GenericQueryAndParameters createQueryAndParameters(Neo4jPersistentEntity<?> entityMetaData,
|
||||
QueryFragmentsAndParameters.QueryFragments queryFragments, Map<String, Object> parameters) {
|
||||
private NodesAndRelationshipsByIdStatementProvider createNodesAndRelationshipsByIdStatementProvider(Neo4jPersistentEntity<?> entityMetaData,
|
||||
QueryFragments queryFragments, Map<String, Object> parameters) {
|
||||
|
||||
// first check if the root node(s) exist(s) at all
|
||||
Statement rootNodesStatement = cypherGenerator
|
||||
@@ -896,7 +898,7 @@ public final class Neo4jTemplate implements Neo4jOperations, FluentNeo4jOperatio
|
||||
|
||||
if (rootNodeIds.isEmpty()) {
|
||||
// fast return if no matching root node(s) are found
|
||||
return GenericQueryAndParameters.EMPTY;
|
||||
return NodesAndRelationshipsByIdStatementProvider.EMPTY;
|
||||
}
|
||||
// load first level relationships
|
||||
final Set<Long> relationshipIds = new HashSet<>();
|
||||
@@ -917,7 +919,7 @@ public final class Neo4jTemplate implements Neo4jOperations, FluentNeo4jOperatio
|
||||
.ifPresent(iterateAndMapNextLevel(relationshipIds, relatedNodeIds, relationshipDescription));
|
||||
}
|
||||
|
||||
return new GenericQueryAndParameters(rootNodeIds, relationshipIds, relatedNodeIds);
|
||||
return new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipIds, relatedNodeIds, queryFragments);
|
||||
}
|
||||
|
||||
private void iterateNextLevel(Collection<Long> nodeIds, Neo4jPersistentEntity<?> target, Set<Long> relationshipIds,
|
||||
|
||||
@@ -58,6 +58,7 @@ import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.mapping.AssociationHandler;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
|
||||
import org.springframework.data.neo4j.core.TemplateSupport.NodesAndRelationshipsByIdStatementProvider;
|
||||
import org.springframework.data.neo4j.core.mapping.Constants;
|
||||
import org.springframework.data.neo4j.core.mapping.CreateRelationshipStatementHolder;
|
||||
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
|
||||
@@ -71,6 +72,7 @@ import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingS
|
||||
import org.springframework.data.neo4j.core.mapping.NodeDescription;
|
||||
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
|
||||
import org.springframework.data.neo4j.core.mapping.callback.ReactiveEventSupport;
|
||||
import org.springframework.data.neo4j.repository.query.QueryFragments;
|
||||
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.ProjectionInformation;
|
||||
@@ -547,21 +549,21 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Rea
|
||||
QueryFragmentsAndParameters queryFragmentsAndParameters) {
|
||||
|
||||
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
|
||||
QueryFragmentsAndParameters.QueryFragments queryFragments = queryFragmentsAndParameters.getQueryFragments();
|
||||
QueryFragments queryFragments = queryFragmentsAndParameters.getQueryFragments();
|
||||
|
||||
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
|
||||
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
|
||||
return createQueryAndParameters(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters())
|
||||
return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters())
|
||||
.flatMap(finalQueryAndParameters ->
|
||||
createExecutableQuery(domainType, renderer.render(queryFragments.generateGenericStatement()),
|
||||
createExecutableQuery(domainType, renderer.render(finalQueryAndParameters.toStatement()),
|
||||
finalQueryAndParameters.getParameters()));
|
||||
}
|
||||
|
||||
return createExecutableQuery(domainType, queryFragments.toStatement(), queryFragmentsAndParameters.getParameters());
|
||||
}
|
||||
|
||||
private Mono<GenericQueryAndParameters> createQueryAndParameters(Neo4jPersistentEntity<?> entityMetaData,
|
||||
QueryFragmentsAndParameters.QueryFragments queryFragments, Map<String, Object> parameters) {
|
||||
private Mono<NodesAndRelationshipsByIdStatementProvider> createNodesAndRelationshipsByIdStatementProvider(Neo4jPersistentEntity<?> entityMetaData,
|
||||
QueryFragments queryFragments, Map<String, Object> parameters) {
|
||||
|
||||
return Mono.deferContextual(ctx -> {
|
||||
Set<Long> rootNodeIds = ctx.get("rootNodes");
|
||||
@@ -590,16 +592,12 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Rea
|
||||
})
|
||||
.expand(iterateAndMapNextLevel(relationshipDescription));
|
||||
})
|
||||
.collect(GenericQueryAndParameters::new, (genericQueryAndParameters, _not_used2) ->
|
||||
genericQueryAndParameters.with(rootNodeIds, processedRelationshipIds, processedNodeIds)
|
||||
);
|
||||
.then(Mono.fromSupplier(() -> new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, processedRelationshipIds, processedNodeIds, queryFragments)));
|
||||
})
|
||||
.contextWrite(ctx -> {
|
||||
return ctx
|
||||
.put("rootNodes", ConcurrentHashMap.newKeySet())
|
||||
.put("processedNodes", ConcurrentHashMap.newKeySet())
|
||||
.put("processedRelationships", ConcurrentHashMap.newKeySet());
|
||||
});
|
||||
.contextWrite(ctx -> ctx
|
||||
.put("rootNodes", ConcurrentHashMap.newKeySet())
|
||||
.put("processedNodes", ConcurrentHashMap.newKeySet())
|
||||
.put("processedRelationships", ConcurrentHashMap.newKeySet()));
|
||||
|
||||
}
|
||||
|
||||
@@ -886,17 +884,18 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Rea
|
||||
String cypherQuery = queryFragmentsAndParameters.getCypherQuery();
|
||||
Map<String, Object> finalParameters = queryFragmentsAndParameters.getParameters();
|
||||
|
||||
QueryFragmentsAndParameters.QueryFragments queryFragments = queryFragmentsAndParameters.getQueryFragments();
|
||||
QueryFragments queryFragments = queryFragmentsAndParameters.getQueryFragments();
|
||||
Neo4jPersistentEntity<?> entityMetaData = (Neo4jPersistentEntity<?>) queryFragmentsAndParameters.getNodeDescription();
|
||||
|
||||
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
|
||||
if (cypherQuery == null || containsPossibleCircles) {
|
||||
|
||||
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
|
||||
return createQueryAndParameters(entityMetaData, queryFragments, finalParameters)
|
||||
.map(genericQueryAndParameters -> {
|
||||
ReactiveNeo4jClient.MappingSpec<T> mappingSpec = this.neo4jClient.query(renderer.render(queryFragments.generateGenericStatement()))
|
||||
.bindAll(genericQueryAndParameters.getParameters()).fetchAs(resultType);
|
||||
return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, finalParameters)
|
||||
.map(nodesAndRelationshipsById -> {
|
||||
ReactiveNeo4jClient.MappingSpec<T> mappingSpec = this.neo4jClient.query(renderer.render(
|
||||
nodesAndRelationshipsById.toStatement()))
|
||||
.bindAll(nodesAndRelationshipsById.getParameters()).fetchAs(resultType);
|
||||
|
||||
ReactiveNeo4jClient.RecordFetchSpec<T> fetchSpec = preparedQuery.getOptionalMappingFunction()
|
||||
.map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)).orElse(mappingSpec);
|
||||
|
||||
@@ -17,6 +17,8 @@ package org.springframework.data.neo4j.core;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -27,7 +29,13 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
import org.neo4j.cypherdsl.core.Functions;
|
||||
import org.neo4j.cypherdsl.core.Node;
|
||||
import org.neo4j.cypherdsl.core.Relationship;
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.springframework.data.neo4j.core.mapping.Constants;
|
||||
import org.springframework.data.neo4j.repository.query.QueryFragments;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -119,6 +127,67 @@ final class TemplateSupport {
|
||||
return mergedParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter holder class for a query with the return pattern of `rootNodes, relationships, relatedNodes`.
|
||||
* The parameter values must be internal node or relationship ids.
|
||||
*/
|
||||
static final class NodesAndRelationshipsByIdStatementProvider {
|
||||
|
||||
private final static String ROOT_NODE_IDS = "rootNodeIds";
|
||||
private final static String RELATIONSHIP_IDS = "relationshipIds";
|
||||
private final static String RELATED_NODE_IDS = "relatedNodeIds";
|
||||
|
||||
final static NodesAndRelationshipsByIdStatementProvider EMPTY =
|
||||
new NodesAndRelationshipsByIdStatementProvider(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), new QueryFragments());
|
||||
|
||||
private final Map<String, Collection<Long>> parameters = new HashMap<>(3);
|
||||
private final QueryFragments queryFragments;
|
||||
|
||||
NodesAndRelationshipsByIdStatementProvider(Collection<Long> rootNodeIds, Collection<Long> relationshipsIds, Collection<Long> relatedNodeIds, QueryFragments queryFragments) {
|
||||
|
||||
this.parameters.put(ROOT_NODE_IDS, rootNodeIds);
|
||||
this.parameters.put(RELATIONSHIP_IDS, relationshipsIds);
|
||||
this.parameters.put(RELATED_NODE_IDS, relatedNodeIds);
|
||||
this.queryFragments = queryFragments;
|
||||
}
|
||||
|
||||
Map<String, Object> getParameters() {
|
||||
return Collections.unmodifiableMap(parameters);
|
||||
}
|
||||
|
||||
boolean hasRootNodeIds() {
|
||||
return parameters.get(ROOT_NODE_IDS).isEmpty();
|
||||
}
|
||||
|
||||
Statement toStatement() {
|
||||
|
||||
String rootNodeIds = "rootNodeIds";
|
||||
String relationshipIds = "relationshipIds";
|
||||
String relatedNodeIds = "relatedNodeIds";
|
||||
Node rootNodes = Cypher.anyNode(rootNodeIds);
|
||||
Node relatedNodes = Cypher.anyNode(relatedNodeIds);
|
||||
Relationship relationships = Cypher.anyNode().relationshipBetween(Cypher.anyNode()).named(relationshipIds);
|
||||
return Cypher.match(rootNodes)
|
||||
.where(Functions.id(rootNodes).in(Cypher.parameter(rootNodeIds)))
|
||||
.optionalMatch(relationships)
|
||||
.where(Functions.id(relationships).in(Cypher.parameter(relationshipIds)))
|
||||
.optionalMatch(relatedNodes)
|
||||
.where(Functions.id(relatedNodes).in(Cypher.parameter(relatedNodeIds)))
|
||||
.with(
|
||||
rootNodes.as(Constants.NAME_OF_ROOT_NODE.getValue()),
|
||||
Functions.collectDistinct(relationships).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
|
||||
Functions.collectDistinct(relatedNodes).as(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES))
|
||||
.orderBy(queryFragments.getOrderBy())
|
||||
.returning(
|
||||
Constants.NAME_OF_ROOT_NODE.as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE),
|
||||
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
|
||||
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)
|
||||
)
|
||||
.skip(queryFragments.getSkip())
|
||||
.limit(queryFragments.getLimit()).build();
|
||||
}
|
||||
}
|
||||
|
||||
private TemplateSupport() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
|
||||
Neo4jPersistentEntity<?> nodeDescription = (Neo4jPersistentEntity<?>) nodeDescriptionStore
|
||||
.getNodeDescription(source.getClass());
|
||||
|
||||
PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(source);
|
||||
PersistentPropertyAccessor<Object> propertyAccessor = nodeDescription.getPropertyAccessor(source);
|
||||
nodeDescription.doWithProperties((Neo4jPersistentProperty p) -> {
|
||||
|
||||
// Skip the internal properties, we don't want them to end up stored as properties
|
||||
|
||||
@@ -256,7 +256,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
@Override
|
||||
protected QueryFragmentsAndParameters complete(@Nullable Condition condition, Sort sort) {
|
||||
|
||||
QueryFragmentsAndParameters.QueryFragments queryFragments = createQueryFragments(condition, sort);
|
||||
QueryFragments queryFragments = createQueryFragments(condition, sort);
|
||||
|
||||
Map<String, Object> convertedParameters = this.boundedParameters.stream()
|
||||
.collect(Collectors.toMap(p -> p.nameOrIndex, p -> parameterConversion.apply(p.value, p.conversionOverride)));
|
||||
@@ -264,8 +264,8 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private QueryFragmentsAndParameters.QueryFragments createQueryFragments(@Nullable Condition condition, Sort sort) {
|
||||
QueryFragmentsAndParameters.QueryFragments queryFragments = new QueryFragmentsAndParameters.QueryFragments();
|
||||
private QueryFragments createQueryFragments(@Nullable Condition condition, Sort sort) {
|
||||
QueryFragments queryFragments = new QueryFragments();
|
||||
|
||||
// all the ways we could query for
|
||||
Node startNode = Cypher.node(nodeDescription.getPrimaryLabel(), nodeDescription.getAdditionalLabels())
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2011-2021 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.repository.query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
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.PatternElement;
|
||||
import org.neo4j.cypherdsl.core.SortItem;
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.neo4j.cypherdsl.core.StatementBuilder;
|
||||
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
|
||||
import org.springframework.data.neo4j.core.mapping.NodeDescription;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Collects the parts of a Cypher query to be handed over to the Cypher generator.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
* @since 6.0.4
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "6.0.4")
|
||||
public final class QueryFragments {
|
||||
private List<PatternElement> matchOn = new ArrayList<>();
|
||||
private Condition condition;
|
||||
private List<Expression> returnExpressions = new ArrayList<>();
|
||||
private SortItem[] orderBy;
|
||||
private Number limit;
|
||||
private Long skip;
|
||||
private ReturnTuple returnTuple;
|
||||
private boolean scalarValueReturn = false;
|
||||
private boolean renderConstantsAsParameters = false;
|
||||
|
||||
public void addMatchOn(PatternElement match) {
|
||||
this.matchOn.add(match);
|
||||
}
|
||||
|
||||
public void setMatchOn(List<PatternElement> match) {
|
||||
this.matchOn = match;
|
||||
}
|
||||
|
||||
public List<PatternElement> getMatchOn() {
|
||||
return matchOn;
|
||||
}
|
||||
|
||||
public void setCondition(@Nullable Condition condition) {
|
||||
this.condition = Optional.ofNullable(condition).orElse(Conditions.noCondition());
|
||||
}
|
||||
|
||||
public Condition getCondition() {
|
||||
return condition;
|
||||
}
|
||||
|
||||
public void setReturnExpressions(Expression[] expression) {
|
||||
this.returnExpressions = Arrays.asList(expression);
|
||||
}
|
||||
|
||||
public void setReturnExpression(Expression returnExpression, boolean isScalarValue) {
|
||||
this.returnExpressions = Collections.singletonList(returnExpression);
|
||||
this.scalarValueReturn = isScalarValue;
|
||||
}
|
||||
|
||||
public boolean includeField(String fieldName) {
|
||||
return this.returnTuple == null || this.returnTuple.includedProperties.isEmpty()
|
||||
|| this.returnTuple.includedProperties.contains(fieldName);
|
||||
}
|
||||
|
||||
public void setOrderBy(SortItem[] orderBy) {
|
||||
this.orderBy = orderBy;
|
||||
}
|
||||
|
||||
public void setLimit(Number limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public void setSkip(Long skip) {
|
||||
this.skip = skip;
|
||||
}
|
||||
|
||||
public void setReturnBasedOn(NodeDescription<?> nodeDescription, List<String> includedProperties,
|
||||
boolean isDistinct) {
|
||||
this.returnTuple = new ReturnTuple(nodeDescription, includedProperties, isDistinct);
|
||||
}
|
||||
|
||||
public ReturnTuple getReturnTuple() {
|
||||
return returnTuple;
|
||||
}
|
||||
|
||||
public boolean isScalarValueReturn() {
|
||||
return scalarValueReturn;
|
||||
}
|
||||
|
||||
public boolean isRenderConstantsAsParameters() {
|
||||
return renderConstantsAsParameters;
|
||||
}
|
||||
|
||||
public void setRenderConstantsAsParameters(boolean renderConstantsAsParameters) {
|
||||
this.renderConstantsAsParameters = renderConstantsAsParameters;
|
||||
}
|
||||
|
||||
public Statement toStatement() {
|
||||
|
||||
StatementBuilder.OngoingReadingWithoutWhere match = null;
|
||||
|
||||
for (PatternElement patternElement : matchOn) {
|
||||
if (match == null) {
|
||||
match = Cypher.match(matchOn.get(0));
|
||||
} else {
|
||||
match = match.match(patternElement);
|
||||
}
|
||||
}
|
||||
|
||||
StatementBuilder.OngoingReadingWithWhere matchWithWhere = match.where(condition);
|
||||
|
||||
StatementBuilder.OngoingReadingAndReturn returnPart = isDistinctReturn()
|
||||
? matchWithWhere.returningDistinct(getReturnExpressions())
|
||||
: matchWithWhere.returning(getReturnExpressions());
|
||||
|
||||
Statement statement = returnPart
|
||||
.orderBy(getOrderBy())
|
||||
.skip(skip)
|
||||
.limit(limit).build();
|
||||
|
||||
statement.setRenderConstantsAsParameters(renderConstantsAsParameters);
|
||||
return statement;
|
||||
}
|
||||
|
||||
private Expression[] getReturnExpressions() {
|
||||
return returnExpressions.size() > 0
|
||||
? returnExpressions.toArray(new Expression[] {})
|
||||
: CypherGenerator.INSTANCE.createReturnStatementForMatch(getReturnTuple().nodeDescription,
|
||||
this::includeField);
|
||||
}
|
||||
|
||||
private boolean isDistinctReturn() {
|
||||
return returnExpressions.isEmpty() && getReturnTuple().isDistinct;
|
||||
}
|
||||
|
||||
public SortItem[] getOrderBy() {
|
||||
return orderBy != null ? orderBy : new SortItem[] {};
|
||||
}
|
||||
|
||||
public Number getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
public Long getSkip() {
|
||||
return skip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes which fields of an entity needs to get returned.
|
||||
*/
|
||||
final static class ReturnTuple {
|
||||
final NodeDescription<?> nodeDescription;
|
||||
final Set<String> includedProperties;
|
||||
final boolean isDistinct;
|
||||
|
||||
private ReturnTuple(NodeDescription<?> nodeDescription, List<String> includedProperties, boolean isDistinct) {
|
||||
this.nodeDescription = nodeDescription;
|
||||
this.includedProperties =
|
||||
includedProperties == null ? Collections.emptySet() : new HashSet<>(includedProperties);
|
||||
this.isDistinct = isDistinct;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,27 +17,14 @@ package org.springframework.data.neo4j.repository.query;
|
||||
|
||||
import static org.neo4j.cypherdsl.core.Cypher.parameter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
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.Functions;
|
||||
import org.neo4j.cypherdsl.core.Node;
|
||||
import org.neo4j.cypherdsl.core.PatternElement;
|
||||
import org.neo4j.cypherdsl.core.Relationship;
|
||||
import org.neo4j.cypherdsl.core.SortItem;
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.neo4j.cypherdsl.core.StatementBuilder;
|
||||
import org.springframework.data.domain.Example;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
@@ -216,172 +203,4 @@ public final class QueryFragmentsAndParameters {
|
||||
|
||||
return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the parts of a Cypher query to be handed over to the Cypher generator.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
* @since 6.0.4
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "6.0.4")
|
||||
public static final class QueryFragments {
|
||||
private List<PatternElement> matchOn = new ArrayList<>();
|
||||
private Condition condition;
|
||||
private List<Expression> returnExpressions = new ArrayList<>();
|
||||
private SortItem[] orderBy;
|
||||
private Number limit;
|
||||
private Long skip;
|
||||
private ReturnTuple returnTuple;
|
||||
private boolean scalarValueReturn = false;
|
||||
private boolean renderConstantsAsParameters = false;
|
||||
|
||||
public void addMatchOn(PatternElement match) {
|
||||
this.matchOn.add(match);
|
||||
}
|
||||
|
||||
public void setMatchOn(List<PatternElement> match) {
|
||||
this.matchOn = match;
|
||||
}
|
||||
|
||||
public List<PatternElement> getMatchOn() {
|
||||
return matchOn;
|
||||
}
|
||||
|
||||
public void setCondition(@Nullable Condition condition) {
|
||||
this.condition = Optional.ofNullable(condition).orElse(Conditions.noCondition());
|
||||
}
|
||||
|
||||
public Condition getCondition() {
|
||||
return condition;
|
||||
}
|
||||
|
||||
public void setReturnExpressions(Expression[] expression) {
|
||||
this.returnExpressions = Arrays.asList(expression);
|
||||
}
|
||||
|
||||
public void setReturnExpression(Expression returnExpression, boolean isScalarValue) {
|
||||
this.returnExpressions = Collections.singletonList(returnExpression);
|
||||
this.scalarValueReturn = isScalarValue;
|
||||
}
|
||||
|
||||
public boolean includeField(String fieldName) {
|
||||
return this.returnTuple == null || this.returnTuple.includedProperties.isEmpty() || this.returnTuple.includedProperties.contains(fieldName);
|
||||
}
|
||||
|
||||
public void setOrderBy(SortItem[] orderBy) {
|
||||
this.orderBy = orderBy;
|
||||
}
|
||||
|
||||
public void setLimit(Number limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public void setSkip(Long skip) {
|
||||
this.skip = skip;
|
||||
}
|
||||
|
||||
public void setReturnBasedOn(NodeDescription<?> nodeDescription, List<String> includedProperties, boolean isDistinct) {
|
||||
this.returnTuple = new ReturnTuple(nodeDescription, includedProperties, isDistinct);
|
||||
}
|
||||
|
||||
public ReturnTuple getReturnTuple() {
|
||||
return returnTuple;
|
||||
}
|
||||
|
||||
public boolean isScalarValueReturn() {
|
||||
return scalarValueReturn;
|
||||
}
|
||||
|
||||
public boolean isRenderConstantsAsParameters() {
|
||||
return renderConstantsAsParameters;
|
||||
}
|
||||
|
||||
public void setRenderConstantsAsParameters(boolean renderConstantsAsParameters) {
|
||||
this.renderConstantsAsParameters = renderConstantsAsParameters;
|
||||
}
|
||||
|
||||
public Statement generateGenericStatement() {
|
||||
String rootNodeIds = "rootNodeIds";
|
||||
String relationshipIds = "relationshipIds";
|
||||
String relatedNodeIds = "relatedNodeIds";
|
||||
Node rootNodes = Cypher.anyNode(rootNodeIds);
|
||||
Node relatedNodes = Cypher.anyNode(relatedNodeIds);
|
||||
Relationship relationships = Cypher.anyNode().relationshipBetween(Cypher.anyNode()).named(relationshipIds);
|
||||
return Cypher.match(rootNodes)
|
||||
.where(Functions.id(rootNodes).in(Cypher.parameter(rootNodeIds)))
|
||||
.optionalMatch(relationships)
|
||||
.where(Functions.id(relationships).in(Cypher.parameter(relationshipIds)))
|
||||
.optionalMatch(relatedNodes)
|
||||
.where(Functions.id(relatedNodes).in(Cypher.parameter(relatedNodeIds)))
|
||||
.with(
|
||||
rootNodes.as(Constants.NAME_OF_ROOT_NODE.getValue()),
|
||||
Functions.collectDistinct(relationships).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
|
||||
Functions.collectDistinct(relatedNodes).as(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES))
|
||||
.orderBy(getOrderBy())
|
||||
.returning(
|
||||
Constants.NAME_OF_ROOT_NODE.as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE),
|
||||
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
|
||||
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)
|
||||
)
|
||||
.skip(skip)
|
||||
.limit(limit).build();
|
||||
}
|
||||
|
||||
public Statement toStatement() {
|
||||
|
||||
StatementBuilder.OngoingReadingWithoutWhere match = null;
|
||||
|
||||
for (PatternElement patternElement : matchOn) {
|
||||
if (match == null) {
|
||||
match = Cypher.match(matchOn.get(0));
|
||||
} else {
|
||||
match = match.match(patternElement);
|
||||
}
|
||||
}
|
||||
|
||||
StatementBuilder.OngoingReadingWithWhere matchWithWhere = match.where(condition);
|
||||
|
||||
StatementBuilder.OngoingReadingAndReturn returnPart = isDistinctReturn()
|
||||
? matchWithWhere.returningDistinct(getReturnExpressions())
|
||||
: matchWithWhere.returning(getReturnExpressions());
|
||||
|
||||
Statement statement = returnPart
|
||||
.orderBy(getOrderBy())
|
||||
.skip(skip)
|
||||
.limit(limit).build();
|
||||
|
||||
statement.setRenderConstantsAsParameters(renderConstantsAsParameters);
|
||||
return statement;
|
||||
}
|
||||
|
||||
private Expression[] getReturnExpressions() {
|
||||
return returnExpressions.size() > 0
|
||||
? returnExpressions.toArray(new Expression[]{})
|
||||
: CypherGenerator.INSTANCE.createReturnStatementForMatch(getReturnTuple().nodeDescription,
|
||||
this::includeField);
|
||||
}
|
||||
|
||||
private boolean isDistinctReturn() {
|
||||
return returnExpressions.isEmpty() && getReturnTuple().isDistinct;
|
||||
}
|
||||
|
||||
private SortItem[] getOrderBy() {
|
||||
return orderBy != null ? orderBy : new SortItem[]{};
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes which fields of an entity needs to get returned.
|
||||
*/
|
||||
final static class ReturnTuple {
|
||||
final NodeDescription<?> nodeDescription;
|
||||
final Set<String> includedProperties;
|
||||
final boolean isDistinct;
|
||||
|
||||
private ReturnTuple(NodeDescription<?> nodeDescription, List<String> includedProperties, boolean isDistinct) {
|
||||
this.nodeDescription = nodeDescription;
|
||||
this.includedProperties = includedProperties == null ? Collections.emptySet() : new HashSet<>(includedProperties);
|
||||
this.isDistinct = isDistinct;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user