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:
Michael Simons
2021-05-18 10:43:25 +02:00
parent 722df847a6
commit c4835f8ee5
8 changed files with 303 additions and 284 deletions

View File

@@ -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();
}
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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() {
}
}

View File

@@ -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

View File

@@ -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())

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}
}