From d9a35eece6aabd62154010487bc47d65c2a367d9 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 9 Mar 2023 11:31:34 +0100 Subject: [PATCH] Support relationships in find by example. It's possible now to also define fields in the relationships to query for by example. This commit advances the functionality of the PropertyPathWrapper class to also cater for the needs of the Predicate created by the example instance(s). Closes #2696 --- .../repository/query/CypherQueryCreator.java | 88 +-------- .../neo4j/repository/query/Predicate.java | 184 +++++++++++++----- .../repository/query/PropertyPathWrapper.java | 122 ++++++++++++ .../query/QueryFragmentsAndParameters.java | 56 ++++-- .../integration/imperative/RepositoryIT.java | 14 +- 5 files changed, 303 insertions(+), 161 deletions(-) create mode 100644 src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java 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 21ce7674b..e6bef515b 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 @@ -35,7 +35,6 @@ import java.util.stream.Stream; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Conditions; import org.neo4j.cypherdsl.core.Cypher; -import org.neo4j.cypherdsl.core.ExposesRelationships; import org.neo4j.cypherdsl.core.Expression; import org.neo4j.cypherdsl.core.Functions; import org.neo4j.cypherdsl.core.Node; @@ -55,7 +54,6 @@ import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Polygon; -import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; import org.springframework.data.neo4j.core.mapping.Constants; @@ -65,8 +63,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.core.mapping.NodeDescription; import org.springframework.data.neo4j.core.mapping.PropertyFilter; -import org.springframework.data.neo4j.core.mapping.RelationshipDescription; -import org.springframework.data.neo4j.core.schema.TargetNode; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; @@ -162,88 +158,6 @@ final class CypherQueryCreator extends AbstractQueryCreator propertyPath; - - PropertyPathWrapper(int index, PersistentPropertyPath propertyPath) { - this.index = index; - this.propertyPath = propertyPath; - } - - public PersistentPropertyPath getPropertyPath() { - return propertyPath; - } - - private String getNodeName() { - return NAME_OF_RELATED_FILTER_ENTITY + "_" + index; - } - - private String getRelationshipName() { - return NAME_OF_RELATED_FILTER_RELATIONSHIP + "_" + index; - } - - private ExposesRelationships createRelationshipChain(ExposesRelationships existingRelationshipChain) { - - ExposesRelationships cypherRelationship = existingRelationshipChain; - int cnt = 0; - for (PersistentProperty persistentProperty : propertyPath) { - - if (persistentProperty.isAssociation() && persistentProperty.isAnnotationPresent(TargetNode.class)) { - break; - } - - RelationshipDescription relationshipDescription = (RelationshipDescription) persistentProperty.getAssociation(); - - if (relationshipDescription == null) { - break; - } - - NodeDescription relationshipPropertiesEntity = relationshipDescription.getRelationshipPropertiesEntity(); - boolean hasTargetNode = hasTargetNode(relationshipPropertiesEntity); - - NodeDescription targetEntity = relationshipDescription.getTarget(); - Node relatedNode = Cypher.node(targetEntity.getPrimaryLabel(), targetEntity.getAdditionalLabels()); - - // length - 1 = last index - // length - 2 = property on last node - // length - 3 = last node itself - boolean lastNode = cnt++ > (propertyPath.getLength() - 3); - if (lastNode || hasTargetNode) { - relatedNode = relatedNode.named(getNodeName()); - } - - cypherRelationship = switch (relationshipDescription.getDirection()) { - case OUTGOING -> cypherRelationship - .relationshipTo(relatedNode, relationshipDescription.getType()); - case INCOMING -> cypherRelationship - .relationshipFrom(relatedNode, relationshipDescription.getType()); - }; - - if (lastNode || hasTargetNode) { - cypherRelationship = ((RelationshipPattern) cypherRelationship).named(getRelationshipName()); - } - } - - return cypherRelationship; - } - - private boolean hasTargetNode(@Nullable NodeDescription relationshipPropertiesEntity) { - return relationshipPropertiesEntity != null - && ((Neo4jPersistentEntity) relationshipPropertiesEntity) - .getPersistentProperty(TargetNode.class) != null; - } - - // if there is no direct property access, the list size is greater than 1 and as a consequence has to contain - // relationships. - private boolean hasRelationships() { - return this.propertyPath.getLength() > 1; - } - } - @Override protected Condition create(Part part, Iterator actualParameters) { return createImpl(part, actualParameters); @@ -592,7 +506,7 @@ final class CypherQueryCreator extends AbstractQueryCreator rp.getPropertyPath().equals(path)).findFirst().get(); + .filter(rp -> rp.getPersistentPropertyPath().equals(path)).findFirst().get(); String cypherElementName; // this "entity" is a representation of a relationship with properties if (owner.isRelationshipPropertiesEntity()) { diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java index c9e309a9c..4f7454ddf 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java @@ -22,8 +22,11 @@ import static org.neo4j.cypherdsl.core.Cypher.property; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import org.neo4j.cypherdsl.core.Condition; @@ -33,6 +36,7 @@ import org.neo4j.cypherdsl.core.Functions; import org.neo4j.cypherdsl.core.StatementBuilder; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.mapping.PropertyPath; import org.springframework.data.neo4j.core.convert.Neo4jConversionService; import org.springframework.data.neo4j.core.mapping.Constants; import org.springframework.data.neo4j.core.mapping.GraphPropertyDescription; @@ -40,8 +44,10 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.core.mapping.NodeDescription; +import org.springframework.data.neo4j.core.mapping.RelationshipDescription; import org.springframework.data.support.ExampleMatcherAccessor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; +import org.springframework.lang.Nullable; /** * Support class for "query by example" executors. @@ -56,80 +62,142 @@ final class Predicate { static Predicate create(Neo4jMappingContext mappingContext, Example example) { - Neo4jPersistentEntity probeNodeDescription = mappingContext.getRequiredPersistentEntity(example.getProbeType()); + Neo4jPersistentEntity nodeDescription = mappingContext.getRequiredPersistentEntity(example.getProbeType()); - Collection graphProperties = probeNodeDescription.getGraphProperties(); + Collection graphProperties = nodeDescription.getGraphProperties(); DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(example.getProbe()); ExampleMatcher matcher = example.getMatcher(); ExampleMatcher.MatchMode mode = matcher.getMatchMode(); ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(matcher); + AtomicInteger relationshipPatternCount = new AtomicInteger(); - Predicate predicate = new Predicate(probeNodeDescription); + Predicate predicate = new Predicate(nodeDescription); for (GraphPropertyDescription graphProperty : graphProperties) { + PropertyPath propertyPath = PropertyPath.from(graphProperty.getFieldName(), nodeDescription.getTypeInformation()); + // create condition for every defined property + PropertyPathWrapper propertyPathWrapper = new PropertyPathWrapper(relationshipPatternCount.incrementAndGet(), mappingContext.getPersistentPropertyPath(propertyPath), true); + addConditionAndParameters(mappingContext, nodeDescription, beanWrapper, mode, matcherAccessor, predicate, graphProperty, propertyPathWrapper); + } - // TODO Relationships are not traversed. + processRelationships(mappingContext, example, nodeDescription, beanWrapper, mode, relationshipPatternCount, null, predicate); - String currentPath = graphProperty.getFieldName(); - if (matcherAccessor.isIgnoredPath(currentPath)) { + return predicate; + } + + private static void processRelationships(Neo4jMappingContext mappingContext, Example example, NodeDescription currentNodeDescription, + DirectFieldAccessFallbackBeanWrapper beanWrapper, ExampleMatcher.MatchMode mode, AtomicInteger relationshipPatternCount, + @Nullable PropertyPath propertyPath, Predicate predicate) { + + for (RelationshipDescription relationship : currentNodeDescription.getRelationships()) { + String relationshipFieldName = relationship.getFieldName(); + Object relationshipObject = beanWrapper.getPropertyValue(relationshipFieldName); + + if (relationshipObject == null) { continue; } - boolean internalId = graphProperty.isIdProperty() && probeNodeDescription.isUsingInternalIds(); - String propertyName = graphProperty.getPropertyName(); - - ExampleMatcher.PropertyValueTransformer transformer = matcherAccessor - .getValueTransformerForPath(currentPath); - Optional optionalValue = transformer - .apply(Optional.ofNullable(beanWrapper.getPropertyValue(currentPath))); - - if (optionalValue.isEmpty()) { - if (!internalId && matcherAccessor.getNullHandler().equals(ExampleMatcher.NullHandler.INCLUDE)) { - predicate.add(mode, property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(probeNodeDescription), propertyName).isNull()); + // Right now we are only accepting the first element of a collection as a filter entry. + // Maybe combining multiple entities with AND might make sense. + if (relationshipObject instanceof Collection collection) { + int collectionSize = collection.size(); + if (collectionSize > 1) { + throw new IllegalArgumentException("Cannot have more than one related node per collection."); } - continue; + if (collectionSize == 0) { + continue; + } + relationshipObject = collection.iterator().next(); + + } + NodeDescription relatedNodeDescription = mappingContext.getNodeDescription(relationshipObject.getClass()); + + // if we come from the root object, the path is probably _null_, + // and it needs to get initialized with the property name of the relationship + PropertyPath nestedPropertyPath = propertyPath == null + ? PropertyPath.from(relationshipFieldName, currentNodeDescription.getUnderlyingClass()) + : propertyPath.nested(relationshipFieldName); + + PropertyPathWrapper nestedPropertyPathWrapper = new PropertyPathWrapper(relationshipPatternCount.incrementAndGet(), mappingContext.getPersistentPropertyPath(nestedPropertyPath), false); + predicate.addRelationship(nestedPropertyPathWrapper); + + for (GraphPropertyDescription graphProperty : relatedNodeDescription.getGraphProperties()) { + addConditionAndParameters(mappingContext, (Neo4jPersistentEntity) relatedNodeDescription, new DirectFieldAccessFallbackBeanWrapper(relationshipObject), mode, + new ExampleMatcherAccessor(example.getMatcher()), predicate, + graphProperty, nestedPropertyPathWrapper); } - Neo4jConversionService conversionService = mappingContext.getConversionService(); + processRelationships(mappingContext, example, relatedNodeDescription, new DirectFieldAccessFallbackBeanWrapper(relationshipObject), mode, relationshipPatternCount, + nestedPropertyPath, predicate); - if (graphProperty.isRelationship()) { - Neo4jQuerySupport.REPOSITORY_QUERY_LOG.error("Querying by example does not support traversing of relationships."); - } else if (graphProperty.isIdProperty() && probeNodeDescription.isUsingInternalIds()) { + } + } + + private static void addConditionAndParameters(Neo4jMappingContext mappingContext, Neo4jPersistentEntity nodeDescription, DirectFieldAccessFallbackBeanWrapper beanWrapper, + ExampleMatcher.MatchMode mode, ExampleMatcherAccessor matcherAccessor, Predicate predicate, GraphPropertyDescription graphProperty, + PropertyPathWrapper wrapper) { + + String currentPath = graphProperty.getFieldName(); + if (matcherAccessor.isIgnoredPath(currentPath)) { + return; + } + + boolean internalId = graphProperty.isIdProperty() && nodeDescription.isUsingInternalIds(); + String propertyName = graphProperty.getPropertyName(); + + ExampleMatcher.PropertyValueTransformer transformer = matcherAccessor + .getValueTransformerForPath(currentPath); + Optional optionalValue = transformer + .apply(Optional.ofNullable(beanWrapper.getPropertyValue(currentPath))); + + if (optionalValue.isEmpty()) { + if (!internalId && matcherAccessor.getNullHandler().equals(ExampleMatcher.NullHandler.INCLUDE)) { + predicate.add(mode, property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), propertyName).isNull()); + } + return; + } + + Neo4jConversionService conversionService = mappingContext.getConversionService(); + boolean isRootNode = predicate.neo4jPersistentEntity.equals(nodeDescription); + + if (graphProperty.isIdProperty() && nodeDescription.isUsingInternalIds()) { + if (isRootNode) { predicate.add(mode, predicate.neo4jPersistentEntity.getIdExpression().isEqualTo(literalOf(optionalValue.get()))); } else { - Expression property = property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(probeNodeDescription), propertyName); - Expression parameter = parameter(propertyName); - Condition condition = property.isEqualTo(parameter); - - if (String.class.equals(graphProperty.getActualType())) { - - if (matcherAccessor.isIgnoreCaseForPath(currentPath)) { - property = Functions.toLower(property); - parameter = Functions.toLower(parameter); - } - - condition = switch (matcherAccessor.getStringMatcherForPath(currentPath)) { - case DEFAULT, EXACT -> - // This needs to be recreated as both property and parameter might have changed above - property.isEqualTo(parameter); - case CONTAINING -> property.contains(parameter); - case STARTING -> property.startsWith(parameter); - case ENDING -> property.endsWith(parameter); - case REGEX -> property.matches(parameter); - }; - } - predicate.add(mode, condition); - predicate.parameters.put(propertyName, optionalValue.map( - v -> { - Neo4jPersistentProperty neo4jPersistentProperty = (Neo4jPersistentProperty) graphProperty; - return conversionService.writeValue(v, neo4jPersistentProperty.getTypeInformation(), - neo4jPersistentProperty.getOptionalConverter()); - }) - .get()); + predicate.add(mode, + nodeDescription.getIdExpression().isEqualTo(literalOf(optionalValue.get()))); } - } + } else { + Expression property = !isRootNode ? property(wrapper.getNodeName(), propertyName) : property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), propertyName); + Expression parameter = parameter(wrapper.getNodeName() + propertyName); + Condition condition = property.isEqualTo(parameter); - return predicate; + if (String.class.equals(graphProperty.getActualType())) { + + if (matcherAccessor.isIgnoreCaseForPath(currentPath)) { + property = Functions.toLower(property); + parameter = Functions.toLower(parameter); + } + + condition = switch (matcherAccessor.getStringMatcherForPath(currentPath)) { + case DEFAULT, EXACT -> + // This needs to be recreated as both property and parameter might have changed above + property.isEqualTo(parameter); + case CONTAINING -> property.contains(parameter); + case STARTING -> property.startsWith(parameter); + case ENDING -> property.endsWith(parameter); + case REGEX -> property.matches(parameter); + }; + } + predicate.add(mode, condition); + predicate.parameters.put(wrapper.getNodeName() + propertyName, optionalValue.map( + v -> { + Neo4jPersistentProperty neo4jPersistentProperty = (Neo4jPersistentProperty) graphProperty; + return conversionService.writeValue(v, neo4jPersistentProperty.getTypeInformation(), + neo4jPersistentProperty.getOptionalConverter()); + }) + .get()); + } } private final Neo4jPersistentEntity neo4jPersistentEntity; @@ -138,6 +206,8 @@ final class Predicate { private final Map parameters = new HashMap<>(); + private final Set relationshipFields = new HashSet<>(); + private Predicate(Neo4jPersistentEntity neo4jPersistentEntity) { this.neo4jPersistentEntity = neo4jPersistentEntity; } @@ -159,6 +229,10 @@ final class Predicate { }; } + private void addRelationship(PropertyPathWrapper propertyPathWrapper) { + this.relationshipFields.add(propertyPathWrapper); + } + public NodeDescription getNeo4jPersistentEntity() { return neo4jPersistentEntity; } @@ -166,4 +240,8 @@ final class Predicate { public Map getParameters() { return Collections.unmodifiableMap(parameters); } + + public Set getPropertyPathWrappers() { + return relationshipFields; + } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java b/src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java new file mode 100644 index 000000000..dff0c4e94 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/query/PropertyPathWrapper.java @@ -0,0 +1,122 @@ +/* + * Copyright 2011-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.repository.query; + +import org.neo4j.cypherdsl.core.Cypher; +import org.neo4j.cypherdsl.core.ExposesRelationships; +import org.neo4j.cypherdsl.core.Node; +import org.neo4j.cypherdsl.core.RelationshipPattern; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.neo4j.core.mapping.NodeDescription; +import org.springframework.data.neo4j.core.mapping.RelationshipDescription; +import org.springframework.data.neo4j.core.schema.TargetNode; +import org.springframework.lang.Nullable; + +class PropertyPathWrapper { + private static final String NAME_OF_RELATED_FILTER_ENTITY = "m"; + private static final String NAME_OF_RELATED_FILTER_RELATIONSHIP = "r"; + + private final int index; + private final PersistentPropertyPath persistentPropertyPath; + private final int lengthModification; + + PropertyPathWrapper(int index, PersistentPropertyPath persistentPropertyPath) { + this(index, persistentPropertyPath, true); + } + + PropertyPathWrapper(int index, PersistentPropertyPath persistentPropertyPath, boolean hasPropertyEnding) { + this.index = index; + this.persistentPropertyPath = persistentPropertyPath; + this.lengthModification = hasPropertyEnding ? 0 : 1; + } + + public PersistentPropertyPath getPersistentPropertyPath() { + return persistentPropertyPath; + } + + String getNodeName() { + return NAME_OF_RELATED_FILTER_ENTITY + "_" + index; + } + + String getRelationshipName() { + return NAME_OF_RELATED_FILTER_RELATIONSHIP + "_" + index; + } + + ExposesRelationships createRelationshipChain(ExposesRelationships existingRelationshipChain) { + + ExposesRelationships cypherRelationship = existingRelationshipChain; + int cnt = 0; + for (PersistentProperty persistentProperty : persistentPropertyPath) { + + if (persistentProperty.isAssociation() && persistentProperty.isAnnotationPresent(TargetNode.class)) { + break; + } + + RelationshipDescription relationshipDescription = (RelationshipDescription) persistentProperty.getAssociation(); + + if (relationshipDescription == null) { + break; + } + + NodeDescription relationshipPropertiesEntity = relationshipDescription.getRelationshipPropertiesEntity(); + boolean isRelationshipPropertiesEntity = isRelationshipPropertiesEntity(relationshipPropertiesEntity); + + NodeDescription targetEntity = relationshipDescription.getTarget(); + Node relatedNode = Cypher.node(targetEntity.getPrimaryLabel(), targetEntity.getAdditionalLabels()); + + // length - 1 = last index + // length - 2 = property on last node + // length - 3 = last node itself + // length + 1 if there is no property ending but the path only goes until it reaches the relationship field + boolean lastNode = cnt > (persistentPropertyPath.getLength() - 3 + lengthModification); + boolean lastRelationship = cnt + 1 > (persistentPropertyPath.getLength() - 4 + lengthModification); + cnt = cnt + 1; + + // we don't yet if the condition will target a relationship property + // that's why here is lastNode or any relationship property + if (lastNode || (isRelationshipPropertiesEntity && lastRelationship)) { + relatedNode = relatedNode.named(getNodeName()); + } + + cypherRelationship = switch (relationshipDescription.getDirection()) { + case OUTGOING -> cypherRelationship + .relationshipTo(relatedNode, relationshipDescription.getType()); + case INCOMING -> cypherRelationship + .relationshipFrom(relatedNode, relationshipDescription.getType()); + }; + + if (lastNode || (isRelationshipPropertiesEntity && lastRelationship)) { + cypherRelationship = ((RelationshipPattern) cypherRelationship).named(getRelationshipName()); + } + } + + return cypherRelationship; + } + + private boolean isRelationshipPropertiesEntity(@Nullable NodeDescription relationshipPropertiesEntity) { + return relationshipPropertiesEntity != null + && ((Neo4jPersistentEntity) relationshipPropertiesEntity) + .getPersistentProperty(TargetNode.class) != null; + } + + // if there is no direct property access, the list size is greater than 1 and as a consequence has to contain + // relationships. + boolean hasRelationships() { + return this.persistentPropertyPath.getLength() > 1; + } +} 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 771f0165e..4f41643a7 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 @@ -15,19 +15,13 @@ */ package org.springframework.data.neo4j.repository.query; -import static org.neo4j.cypherdsl.core.Cypher.parameter; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; - 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.Node; +import org.neo4j.cypherdsl.core.PatternElement; +import org.neo4j.cypherdsl.core.RelationshipPattern; import org.neo4j.cypherdsl.core.SortItem; import org.springframework.data.domain.Example; import org.springframework.data.domain.Pageable; @@ -40,6 +34,15 @@ import org.springframework.data.neo4j.core.mapping.NodeDescription; import org.springframework.data.neo4j.core.mapping.PropertyFilter; import org.springframework.lang.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.neo4j.cypherdsl.core.Cypher.parameter; + /** * Combines the QueryFragments with parameters. * @@ -201,7 +204,7 @@ public final class QueryFragmentsAndParameters { * Utility method for creating a query fragment including parameters for a given condition. * * @param entityMetaData The metadata of a given and known entity - * @param condition A Cypher-DSL condition + * @param condition A Cypher-DSL condition * @return Fully populated fragments and parameter */ @API(status = API.Status.EXPERIMENTAL, since = "6.1.7") @@ -210,18 +213,18 @@ public final class QueryFragmentsAndParameters { } static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, - Condition condition, - @Nullable Pageable pageable, - @Nullable Collection sortItems + Condition condition, + @Nullable Pageable pageable, + @Nullable Collection sortItems ) { return forCondition(entityMetaData, condition, pageable, sortItems, null); } static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, - Condition condition, - @Nullable Pageable pageable, - @Nullable Collection sortItems, - @Nullable java.util.function.Predicate includeField + Condition condition, + @Nullable Pageable pageable, + @Nullable Collection sortItems, + @Nullable java.util.function.Predicate includeField ) { QueryFragments queryFragments = new QueryFragments(); @@ -260,25 +263,38 @@ public final class QueryFragmentsAndParameters { Predicate predicate = Predicate.create(mappingContext, example); Map parameters = predicate.getParameters(); + Set propertyPathWrappers = predicate.getPropertyPathWrappers(); Condition condition = predicate.getCondition(); return getQueryFragmentsAndParameters(mappingContext.getPersistentEntity(example.getProbeType()), pageable, - sort, parameters, condition, includeField); + sort, parameters, condition, includeField, propertyPathWrappers); } public static QueryFragmentsAndParameters forPageableAndSort(Neo4jPersistentEntity neo4jPersistentEntity, @Nullable Pageable pageable, @Nullable Sort sort) { - return getQueryFragmentsAndParameters(neo4jPersistentEntity, pageable, sort, Collections.emptyMap(), null, null); + return getQueryFragmentsAndParameters(neo4jPersistentEntity, pageable, sort, Collections.emptyMap(), null, null, null); } private static QueryFragmentsAndParameters getQueryFragmentsAndParameters( Neo4jPersistentEntity entityMetaData, @Nullable Pageable pageable, @Nullable Sort sort, @Nullable Map parameters, @Nullable Condition condition, @Nullable - java.util.function.Predicate includeField) { + java.util.function.Predicate includeField, + @Nullable Set propertyPathWrappers) { QueryFragments queryFragments = new QueryFragments(); - queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData)); + + if (propertyPathWrappers != null && !propertyPathWrappers.isEmpty()) { + Node startNode = Cypher.node(entityMetaData.getPrimaryLabel(), entityMetaData.getAdditionalLabels()) + .named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)); + List relationshipChain = new ArrayList<>(); + for (PropertyPathWrapper possiblePathWithRelationship : propertyPathWrappers) { + relationshipChain.add((RelationshipPattern) possiblePathWithRelationship.createRelationshipChain(startNode)); + } + queryFragments.setMatchOn(relationshipChain); + } else { + queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData)); + } queryFragments.setCondition(condition); if (includeField == null) { queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData)); diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java index 90421321c..38a1a7e47 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java @@ -3038,7 +3038,8 @@ class RepositoryIT { CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), (n)-[:Has]->(p1:Pet{name: 'Jerry'}), - (n)-[:Has]->(p2:Pet{name: 'Tom'}) + (n)-[:Has]->(p2:Pet{name: 'Tom'}), + (p1)-[:Has]->(p3:Pet{name: 'Silvester'})-[:Has]->(h2:Hobby{name: 'Hunt Tweety'}) RETURN n, h1, p1, p2 """).single()); @@ -3054,6 +3055,17 @@ class RepositoryIT { PersonWithRelationship probe = new PersonWithRelationship(); probe.setName("Freddie"); + Hobby hobbies = new Hobby(); + hobbies.setName("Music"); + probe.setHobbies(hobbies); + Pet jerry = new Pet("Jerry"); + // yes, now we bring multiple universes together + Pet silvester = new Pet("Silvester"); + Hobby silvesterHobby = new Hobby(); + silvesterHobby.setName("Hunt Tweety"); + silvester.setHobbies(Set.of(silvesterHobby)); + jerry.setFriends(List.of(silvester)); + probe.setPets(List.of(jerry)); PersonWithRelationship loadedPerson = repository.findAll(Example.of(probe)).get(0); assertThat(loadedPerson.getName()).isEqualTo("Freddie"); assertThat(loadedPerson.getId()).isEqualTo(personId);