Skip to content

Commit d9a35ee

Browse files
committed
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
1 parent 4c65539 commit d9a35ee

File tree

5 files changed

+301
-159
lines changed

5 files changed

+301
-159
lines changed

src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java

+1-87
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
import org.neo4j.cypherdsl.core.Condition;
3636
import org.neo4j.cypherdsl.core.Conditions;
3737
import org.neo4j.cypherdsl.core.Cypher;
38-
import org.neo4j.cypherdsl.core.ExposesRelationships;
3938
import org.neo4j.cypherdsl.core.Expression;
4039
import org.neo4j.cypherdsl.core.Functions;
4140
import org.neo4j.cypherdsl.core.Node;
@@ -55,7 +54,6 @@
5554
import org.springframework.data.geo.Circle;
5655
import org.springframework.data.geo.Distance;
5756
import org.springframework.data.geo.Polygon;
58-
import org.springframework.data.mapping.PersistentProperty;
5957
import org.springframework.data.mapping.PersistentPropertyPath;
6058
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
6159
import org.springframework.data.neo4j.core.mapping.Constants;
@@ -65,8 +63,6 @@
6563
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
6664
import org.springframework.data.neo4j.core.mapping.NodeDescription;
6765
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
68-
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
69-
import org.springframework.data.neo4j.core.schema.TargetNode;
7066
import org.springframework.data.repository.query.QueryMethod;
7167
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
7268
import org.springframework.data.repository.query.parser.Part;
@@ -162,88 +158,6 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
162158
this.keysetRequiresSort = queryMethod.isScrollQuery() && actualParameters.getScrollPosition() instanceof KeysetScrollPosition;
163159
}
164160

165-
private class PropertyPathWrapper {
166-
private static final String NAME_OF_RELATED_FILTER_ENTITY = "m";
167-
private static final String NAME_OF_RELATED_FILTER_RELATIONSHIP = "r";
168-
169-
private final int index;
170-
private final PersistentPropertyPath<?> propertyPath;
171-
172-
PropertyPathWrapper(int index, PersistentPropertyPath<?> propertyPath) {
173-
this.index = index;
174-
this.propertyPath = propertyPath;
175-
}
176-
177-
public PersistentPropertyPath<?> getPropertyPath() {
178-
return propertyPath;
179-
}
180-
181-
private String getNodeName() {
182-
return NAME_OF_RELATED_FILTER_ENTITY + "_" + index;
183-
}
184-
185-
private String getRelationshipName() {
186-
return NAME_OF_RELATED_FILTER_RELATIONSHIP + "_" + index;
187-
}
188-
189-
private ExposesRelationships<?> createRelationshipChain(ExposesRelationships<?> existingRelationshipChain) {
190-
191-
ExposesRelationships<?> cypherRelationship = existingRelationshipChain;
192-
int cnt = 0;
193-
for (PersistentProperty<?> persistentProperty : propertyPath) {
194-
195-
if (persistentProperty.isAssociation() && persistentProperty.isAnnotationPresent(TargetNode.class)) {
196-
break;
197-
}
198-
199-
RelationshipDescription relationshipDescription = (RelationshipDescription) persistentProperty.getAssociation();
200-
201-
if (relationshipDescription == null) {
202-
break;
203-
}
204-
205-
NodeDescription<?> relationshipPropertiesEntity = relationshipDescription.getRelationshipPropertiesEntity();
206-
boolean hasTargetNode = hasTargetNode(relationshipPropertiesEntity);
207-
208-
NodeDescription<?> targetEntity = relationshipDescription.getTarget();
209-
Node relatedNode = Cypher.node(targetEntity.getPrimaryLabel(), targetEntity.getAdditionalLabels());
210-
211-
// length - 1 = last index
212-
// length - 2 = property on last node
213-
// length - 3 = last node itself
214-
boolean lastNode = cnt++ > (propertyPath.getLength() - 3);
215-
if (lastNode || hasTargetNode) {
216-
relatedNode = relatedNode.named(getNodeName());
217-
}
218-
219-
cypherRelationship = switch (relationshipDescription.getDirection()) {
220-
case OUTGOING -> cypherRelationship
221-
.relationshipTo(relatedNode, relationshipDescription.getType());
222-
case INCOMING -> cypherRelationship
223-
.relationshipFrom(relatedNode, relationshipDescription.getType());
224-
};
225-
226-
if (lastNode || hasTargetNode) {
227-
cypherRelationship = ((RelationshipPattern) cypherRelationship).named(getRelationshipName());
228-
}
229-
}
230-
231-
return cypherRelationship;
232-
}
233-
234-
private boolean hasTargetNode(@Nullable NodeDescription<?> relationshipPropertiesEntity) {
235-
return relationshipPropertiesEntity != null
236-
&& ((Neo4jPersistentEntity<?>) relationshipPropertiesEntity)
237-
.getPersistentProperty(TargetNode.class) != null;
238-
}
239-
240-
// if there is no direct property access, the list size is greater than 1 and as a consequence has to contain
241-
// relationships.
242-
private boolean hasRelationships() {
243-
return this.propertyPath.getLength() > 1;
244-
}
245-
}
246-
247161
@Override
248162
protected Condition create(Part part, Iterator<Object> actualParameters) {
249163
return createImpl(part, actualParameters);
@@ -592,7 +506,7 @@ private String getContainerName(PersistentPropertyPath<Neo4jPersistentProperty>
592506
}
593507

594508
PropertyPathWrapper propertyPathWrapper = propertyPathWrappers.stream()
595-
.filter(rp -> rp.getPropertyPath().equals(path)).findFirst().get();
509+
.filter(rp -> rp.getPersistentPropertyPath().equals(path)).findFirst().get();
596510
String cypherElementName;
597511
// this "entity" is a representation of a relationship with properties
598512
if (owner.isRelationshipPropertiesEntity()) {

src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java

+129-51
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@
2222
import java.util.Collection;
2323
import java.util.Collections;
2424
import java.util.HashMap;
25+
import java.util.HashSet;
2526
import java.util.Map;
2627
import java.util.Optional;
28+
import java.util.Set;
29+
import java.util.concurrent.atomic.AtomicInteger;
2730
import java.util.function.BiFunction;
2831

2932
import org.neo4j.cypherdsl.core.Condition;
@@ -33,15 +36,18 @@
3336
import org.neo4j.cypherdsl.core.StatementBuilder;
3437
import org.springframework.data.domain.Example;
3538
import org.springframework.data.domain.ExampleMatcher;
39+
import org.springframework.data.mapping.PropertyPath;
3640
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
3741
import org.springframework.data.neo4j.core.mapping.Constants;
3842
import org.springframework.data.neo4j.core.mapping.GraphPropertyDescription;
3943
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
4044
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
4145
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
4246
import org.springframework.data.neo4j.core.mapping.NodeDescription;
47+
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
4348
import org.springframework.data.support.ExampleMatcherAccessor;
4449
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
50+
import org.springframework.lang.Nullable;
4551

4652
/**
4753
* Support class for "query by example" executors.
@@ -56,80 +62,142 @@ final class Predicate {
5662

5763
static <S> Predicate create(Neo4jMappingContext mappingContext, Example<S> example) {
5864

59-
Neo4jPersistentEntity<?> probeNodeDescription = mappingContext.getRequiredPersistentEntity(example.getProbeType());
65+
Neo4jPersistentEntity<?> nodeDescription = mappingContext.getRequiredPersistentEntity(example.getProbeType());
6066

61-
Collection<GraphPropertyDescription> graphProperties = probeNodeDescription.getGraphProperties();
67+
Collection<GraphPropertyDescription> graphProperties = nodeDescription.getGraphProperties();
6268
DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(example.getProbe());
6369
ExampleMatcher matcher = example.getMatcher();
6470
ExampleMatcher.MatchMode mode = matcher.getMatchMode();
6571
ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(matcher);
72+
AtomicInteger relationshipPatternCount = new AtomicInteger();
6673

67-
Predicate predicate = new Predicate(probeNodeDescription);
74+
Predicate predicate = new Predicate(nodeDescription);
6875
for (GraphPropertyDescription graphProperty : graphProperties) {
76+
PropertyPath propertyPath = PropertyPath.from(graphProperty.getFieldName(), nodeDescription.getTypeInformation());
77+
// create condition for every defined property
78+
PropertyPathWrapper propertyPathWrapper = new PropertyPathWrapper(relationshipPatternCount.incrementAndGet(), mappingContext.getPersistentPropertyPath(propertyPath), true);
79+
addConditionAndParameters(mappingContext, nodeDescription, beanWrapper, mode, matcherAccessor, predicate, graphProperty, propertyPathWrapper);
80+
}
81+
82+
processRelationships(mappingContext, example, nodeDescription, beanWrapper, mode, relationshipPatternCount, null, predicate);
83+
84+
return predicate;
85+
}
6986

70-
// TODO Relationships are not traversed.
87+
private static <S> void processRelationships(Neo4jMappingContext mappingContext, Example<S> example, NodeDescription<?> currentNodeDescription,
88+
DirectFieldAccessFallbackBeanWrapper beanWrapper, ExampleMatcher.MatchMode mode, AtomicInteger relationshipPatternCount,
89+
@Nullable PropertyPath propertyPath, Predicate predicate) {
7190

72-
String currentPath = graphProperty.getFieldName();
73-
if (matcherAccessor.isIgnoredPath(currentPath)) {
91+
for (RelationshipDescription relationship : currentNodeDescription.getRelationships()) {
92+
String relationshipFieldName = relationship.getFieldName();
93+
Object relationshipObject = beanWrapper.getPropertyValue(relationshipFieldName);
94+
95+
if (relationshipObject == null) {
7496
continue;
7597
}
7698

77-
boolean internalId = graphProperty.isIdProperty() && probeNodeDescription.isUsingInternalIds();
78-
String propertyName = graphProperty.getPropertyName();
99+
// Right now we are only accepting the first element of a collection as a filter entry.
100+
// Maybe combining multiple entities with AND might make sense.
101+
if (relationshipObject instanceof Collection collection) {
102+
int collectionSize = collection.size();
103+
if (collectionSize > 1) {
104+
throw new IllegalArgumentException("Cannot have more than one related node per collection.");
105+
}
106+
if (collectionSize == 0) {
107+
continue;
108+
}
109+
relationshipObject = collection.iterator().next();
79110

80-
ExampleMatcher.PropertyValueTransformer transformer = matcherAccessor
81-
.getValueTransformerForPath(currentPath);
82-
Optional<Object> optionalValue = transformer
83-
.apply(Optional.ofNullable(beanWrapper.getPropertyValue(currentPath)));
111+
}
112+
NodeDescription<?> relatedNodeDescription = mappingContext.getNodeDescription(relationshipObject.getClass());
84113

85-
if (optionalValue.isEmpty()) {
86-
if (!internalId && matcherAccessor.getNullHandler().equals(ExampleMatcher.NullHandler.INCLUDE)) {
87-
predicate.add(mode, property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(probeNodeDescription), propertyName).isNull());
88-
}
89-
continue;
114+
// if we come from the root object, the path is probably _null_,
115+
// and it needs to get initialized with the property name of the relationship
116+
PropertyPath nestedPropertyPath = propertyPath == null
117+
? PropertyPath.from(relationshipFieldName, currentNodeDescription.getUnderlyingClass())
118+
: propertyPath.nested(relationshipFieldName);
119+
120+
PropertyPathWrapper nestedPropertyPathWrapper = new PropertyPathWrapper(relationshipPatternCount.incrementAndGet(), mappingContext.getPersistentPropertyPath(nestedPropertyPath), false);
121+
predicate.addRelationship(nestedPropertyPathWrapper);
122+
123+
for (GraphPropertyDescription graphProperty : relatedNodeDescription.getGraphProperties()) {
124+
addConditionAndParameters(mappingContext, (Neo4jPersistentEntity<?>) relatedNodeDescription, new DirectFieldAccessFallbackBeanWrapper(relationshipObject), mode,
125+
new ExampleMatcherAccessor(example.getMatcher()), predicate,
126+
graphProperty, nestedPropertyPathWrapper);
90127
}
91128

92-
Neo4jConversionService conversionService = mappingContext.getConversionService();
129+
processRelationships(mappingContext, example, relatedNodeDescription, new DirectFieldAccessFallbackBeanWrapper(relationshipObject), mode, relationshipPatternCount,
130+
nestedPropertyPath, predicate);
131+
132+
}
133+
}
134+
135+
private static void addConditionAndParameters(Neo4jMappingContext mappingContext, Neo4jPersistentEntity<?> nodeDescription, DirectFieldAccessFallbackBeanWrapper beanWrapper,
136+
ExampleMatcher.MatchMode mode, ExampleMatcherAccessor matcherAccessor, Predicate predicate, GraphPropertyDescription graphProperty,
137+
PropertyPathWrapper wrapper) {
138+
139+
String currentPath = graphProperty.getFieldName();
140+
if (matcherAccessor.isIgnoredPath(currentPath)) {
141+
return;
142+
}
93143

94-
if (graphProperty.isRelationship()) {
95-
Neo4jQuerySupport.REPOSITORY_QUERY_LOG.error("Querying by example does not support traversing of relationships.");
96-
} else if (graphProperty.isIdProperty() && probeNodeDescription.isUsingInternalIds()) {
144+
boolean internalId = graphProperty.isIdProperty() && nodeDescription.isUsingInternalIds();
145+
String propertyName = graphProperty.getPropertyName();
146+
147+
ExampleMatcher.PropertyValueTransformer transformer = matcherAccessor
148+
.getValueTransformerForPath(currentPath);
149+
Optional<Object> optionalValue = transformer
150+
.apply(Optional.ofNullable(beanWrapper.getPropertyValue(currentPath)));
151+
152+
if (optionalValue.isEmpty()) {
153+
if (!internalId && matcherAccessor.getNullHandler().equals(ExampleMatcher.NullHandler.INCLUDE)) {
154+
predicate.add(mode, property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), propertyName).isNull());
155+
}
156+
return;
157+
}
158+
159+
Neo4jConversionService conversionService = mappingContext.getConversionService();
160+
boolean isRootNode = predicate.neo4jPersistentEntity.equals(nodeDescription);
161+
162+
if (graphProperty.isIdProperty() && nodeDescription.isUsingInternalIds()) {
163+
if (isRootNode) {
97164
predicate.add(mode,
98165
predicate.neo4jPersistentEntity.getIdExpression().isEqualTo(literalOf(optionalValue.get())));
99166
} else {
100-
Expression property = property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(probeNodeDescription), propertyName);
101-
Expression parameter = parameter(propertyName);
102-
Condition condition = property.isEqualTo(parameter);
103-
104-
if (String.class.equals(graphProperty.getActualType())) {
105-
106-
if (matcherAccessor.isIgnoreCaseForPath(currentPath)) {
107-
property = Functions.toLower(property);
108-
parameter = Functions.toLower(parameter);
109-
}
110-
111-
condition = switch (matcherAccessor.getStringMatcherForPath(currentPath)) {
112-
case DEFAULT, EXACT ->
113-
// This needs to be recreated as both property and parameter might have changed above
114-
property.isEqualTo(parameter);
115-
case CONTAINING -> property.contains(parameter);
116-
case STARTING -> property.startsWith(parameter);
117-
case ENDING -> property.endsWith(parameter);
118-
case REGEX -> property.matches(parameter);
119-
};
167+
predicate.add(mode,
168+
nodeDescription.getIdExpression().isEqualTo(literalOf(optionalValue.get())));
169+
}
170+
} else {
171+
Expression property = !isRootNode ? property(wrapper.getNodeName(), propertyName) : property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), propertyName);
172+
Expression parameter = parameter(wrapper.getNodeName() + propertyName);
173+
Condition condition = property.isEqualTo(parameter);
174+
175+
if (String.class.equals(graphProperty.getActualType())) {
176+
177+
if (matcherAccessor.isIgnoreCaseForPath(currentPath)) {
178+
property = Functions.toLower(property);
179+
parameter = Functions.toLower(parameter);
120180
}
121-
predicate.add(mode, condition);
122-
predicate.parameters.put(propertyName, optionalValue.map(
123-
v -> {
124-
Neo4jPersistentProperty neo4jPersistentProperty = (Neo4jPersistentProperty) graphProperty;
125-
return conversionService.writeValue(v, neo4jPersistentProperty.getTypeInformation(),
126-
neo4jPersistentProperty.getOptionalConverter());
127-
})
128-
.get());
181+
182+
condition = switch (matcherAccessor.getStringMatcherForPath(currentPath)) {
183+
case DEFAULT, EXACT ->
184+
// This needs to be recreated as both property and parameter might have changed above
185+
property.isEqualTo(parameter);
186+
case CONTAINING -> property.contains(parameter);
187+
case STARTING -> property.startsWith(parameter);
188+
case ENDING -> property.endsWith(parameter);
189+
case REGEX -> property.matches(parameter);
190+
};
129191
}
192+
predicate.add(mode, condition);
193+
predicate.parameters.put(wrapper.getNodeName() + propertyName, optionalValue.map(
194+
v -> {
195+
Neo4jPersistentProperty neo4jPersistentProperty = (Neo4jPersistentProperty) graphProperty;
196+
return conversionService.writeValue(v, neo4jPersistentProperty.getTypeInformation(),
197+
neo4jPersistentProperty.getOptionalConverter());
198+
})
199+
.get());
130200
}
131-
132-
return predicate;
133201
}
134202

135203
private final Neo4jPersistentEntity neo4jPersistentEntity;
@@ -138,6 +206,8 @@ static <S> Predicate create(Neo4jMappingContext mappingContext, Example<S> examp
138206

139207
private final Map<String, Object> parameters = new HashMap<>();
140208

209+
private final Set<PropertyPathWrapper> relationshipFields = new HashSet<>();
210+
141211
private Predicate(Neo4jPersistentEntity neo4jPersistentEntity) {
142212
this.neo4jPersistentEntity = neo4jPersistentEntity;
143213
}
@@ -159,11 +229,19 @@ private void add(ExampleMatcher.MatchMode matchMode, Condition additionalConditi
159229
};
160230
}
161231

232+
private void addRelationship(PropertyPathWrapper propertyPathWrapper) {
233+
this.relationshipFields.add(propertyPathWrapper);
234+
}
235+
162236
public NodeDescription<?> getNeo4jPersistentEntity() {
163237
return neo4jPersistentEntity;
164238
}
165239

166240
public Map<String, Object> getParameters() {
167241
return Collections.unmodifiableMap(parameters);
168242
}
243+
244+
public Set<PropertyPathWrapper> getPropertyPathWrappers() {
245+
return relationshipFields;
246+
}
169247
}

0 commit comments

Comments
 (0)