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