diff --git a/src/main/java/org/springframework/data/neo4j/core/GenericQueryAndParameters.java b/src/main/java/org/springframework/data/neo4j/core/GenericQueryAndParameters.java deleted file mode 100644 index 165710321..000000000 --- a/src/main/java/org/springframework/data/neo4j/core/GenericQueryAndParameters.java +++ /dev/null @@ -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> parameters = new HashMap<>(3); - - GenericQueryAndParameters(Collection rootNodeIds, Collection relationshipsIds, Collection 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 rootNodeIds, Collection relationshipsIds, Collection relatedNodeIds) { - parameters.put(ROOT_NODE_IDS, rootNodeIds); - parameters.put(RELATIONSHIP_IDS, relationshipsIds); - parameters.put(RELATED_NODE_IDS, relatedNodeIds); - } - - Map getParameters() { - return Collections.unmodifiableMap(parameters); - } - - boolean isEmpty() { - return parameters.get(ROOT_NODE_IDS).isEmpty(); - } - -} diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index c35637e02..e54cfb9bf 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -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 DynamicLabels determineDynamicLabels(T entityToBeSaved, Neo4jPersistentEntity entityMetaData) { return entityMetaData.getDynamicLabelsProperty().map(p -> { - PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved); + PersistentPropertyAccessor 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 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> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass); - List> entityList = entitiesToBeSaved.stream().map(h -> h.t3).map(binderFunction) + List> 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 propertyAccessor = entityMetaData.getPropertyAccessor(t.t3); - return processRelations(entityMetaData, t.t1, propertyAccessor, t.t2, TemplateSupport.computeIncludePropertyPredicate(includedProperties)); + PersistentPropertyAccessor 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 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 parameters) { + private NodesAndRelationshipsByIdStatementProvider createNodesAndRelationshipsByIdStatementProvider(Neo4jPersistentEntity entityMetaData, + QueryFragments queryFragments, Map 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 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 nodeIds, Neo4jPersistentEntity target, Set relationshipIds, diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java index 7e16f8e83..8ba33596c 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java @@ -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 createQueryAndParameters(Neo4jPersistentEntity entityMetaData, - QueryFragmentsAndParameters.QueryFragments queryFragments, Map parameters) { + private Mono createNodesAndRelationshipsByIdStatementProvider(Neo4jPersistentEntity entityMetaData, + QueryFragments queryFragments, Map parameters) { return Mono.deferContextual(ctx -> { Set 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 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 mappingSpec = this.neo4jClient.query(renderer.render(queryFragments.generateGenericStatement())) - .bindAll(genericQueryAndParameters.getParameters()).fetchAs(resultType); + return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, finalParameters) + .map(nodesAndRelationshipsById -> { + ReactiveNeo4jClient.MappingSpec mappingSpec = this.neo4jClient.query(renderer.render( + nodesAndRelationshipsById.toStatement())) + .bindAll(nodesAndRelationshipsById.getParameters()).fetchAs(resultType); ReactiveNeo4jClient.RecordFetchSpec fetchSpec = preparedQuery.getOptionalMappingFunction() .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)).orElse(mappingSpec); diff --git a/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java b/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java index 924972650..f858c51b6 100644 --- a/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java @@ -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> parameters = new HashMap<>(3); + private final QueryFragments queryFragments; + + NodesAndRelationshipsByIdStatementProvider(Collection rootNodeIds, Collection relationshipsIds, Collection 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 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() { } } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java index c4c60f033..de90d7464 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java @@ -175,7 +175,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { Neo4jPersistentEntity nodeDescription = (Neo4jPersistentEntity) nodeDescriptionStore .getNodeDescription(source.getClass()); - PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(source); + PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(source); nodeDescription.doWithProperties((Neo4jPersistentProperty p) -> { // Skip the internal properties, we don't want them to end up stored as properties diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 43d4220d3..fa8ed3249 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -256,7 +256,7 @@ final class CypherQueryCreator extends AbstractQueryCreator 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 matchOn = new ArrayList<>(); + private Condition condition; + private List 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 match) { + this.matchOn = match; + } + + public List 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 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 includedProperties; + final boolean isDistinct; + + private ReturnTuple(NodeDescription nodeDescription, List includedProperties, boolean isDistinct) { + this.nodeDescription = nodeDescription; + this.includedProperties = + includedProperties == null ? Collections.emptySet() : new HashSet<>(includedProperties); + this.isDistinct = isDistinct; + } + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java index fc493307e..4c0e5f96a 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java @@ -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 matchOn = new ArrayList<>(); - private Condition condition; - private List 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 match) { - this.matchOn = match; - } - - public List 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 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 includedProperties; - final boolean isDistinct; - - private ReturnTuple(NodeDescription nodeDescription, List includedProperties, boolean isDistinct) { - this.nodeDescription = nodeDescription; - this.includedProperties = includedProperties == null ? Collections.emptySet() : new HashSet<>(includedProperties); - this.isDistinct = isDistinct; - } - } - } }