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
This commit is contained in:
@@ -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<QueryFragmentsAndPar
|
||||
this.keysetRequiresSort = queryMethod.isScrollQuery() && actualParameters.getScrollPosition() instanceof KeysetScrollPosition;
|
||||
}
|
||||
|
||||
private 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<?> 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<Object> actualParameters) {
|
||||
return createImpl(part, actualParameters);
|
||||
@@ -592,7 +506,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
|
||||
}
|
||||
|
||||
PropertyPathWrapper propertyPathWrapper = propertyPathWrappers.stream()
|
||||
.filter(rp -> 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()) {
|
||||
|
||||
@@ -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 <S> Predicate create(Neo4jMappingContext mappingContext, Example<S> example) {
|
||||
|
||||
Neo4jPersistentEntity<?> probeNodeDescription = mappingContext.getRequiredPersistentEntity(example.getProbeType());
|
||||
Neo4jPersistentEntity<?> nodeDescription = mappingContext.getRequiredPersistentEntity(example.getProbeType());
|
||||
|
||||
Collection<GraphPropertyDescription> graphProperties = probeNodeDescription.getGraphProperties();
|
||||
Collection<GraphPropertyDescription> 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 <S> void processRelationships(Neo4jMappingContext mappingContext, Example<S> 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<Object> 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<Object> 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<String, Object> parameters = new HashMap<>();
|
||||
|
||||
private final Set<PropertyPathWrapper> 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<String, Object> getParameters() {
|
||||
return Collections.unmodifiableMap(parameters);
|
||||
}
|
||||
|
||||
public Set<PropertyPathWrapper> getPropertyPathWrappers() {
|
||||
return relationshipFields;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<SortItem> sortItems
|
||||
Condition condition,
|
||||
@Nullable Pageable pageable,
|
||||
@Nullable Collection<SortItem> sortItems
|
||||
) {
|
||||
return forCondition(entityMetaData, condition, pageable, sortItems, null);
|
||||
}
|
||||
|
||||
static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity<?> entityMetaData,
|
||||
Condition condition,
|
||||
@Nullable Pageable pageable,
|
||||
@Nullable Collection<SortItem> sortItems,
|
||||
@Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField
|
||||
Condition condition,
|
||||
@Nullable Pageable pageable,
|
||||
@Nullable Collection<SortItem> sortItems,
|
||||
@Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField
|
||||
) {
|
||||
|
||||
QueryFragments queryFragments = new QueryFragments();
|
||||
@@ -260,25 +263,38 @@ public final class QueryFragmentsAndParameters {
|
||||
|
||||
Predicate predicate = Predicate.create(mappingContext, example);
|
||||
Map<String, Object> parameters = predicate.getParameters();
|
||||
Set<PropertyPathWrapper> 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<String, Object> parameters, @Nullable Condition condition, @Nullable
|
||||
java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
|
||||
java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField,
|
||||
@Nullable Set<PropertyPathWrapper> 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<PatternElement> 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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user