Skip to content

Commit 02068ed

Browse files
GH-2703 - Add support for negating transformers to query-by-example.
Closes #2703.
1 parent f5495d4 commit 02068ed

File tree

5 files changed

+144
-19
lines changed

5 files changed

+144
-19
lines changed

src/main/asciidoc/faq/index.adoc

+25-5
Original file line numberDiff line numberDiff line change
@@ -1277,17 +1277,37 @@ Example<MovieEntity> movieExample = Example.of(new MovieEntity("The Matrix", nul
12771277
Flux<MovieEntity> movies = this.movieRepository.findAll(movieExample);
12781278
12791279
movieExample = Example.of(
1280-
new MovieEntity("Matrix", null),
1281-
ExampleMatcher
1282-
.matchingAny()
1280+
new MovieEntity("Matrix", null),
1281+
ExampleMatcher
1282+
.matchingAny()
12831283
.withMatcher(
1284-
"title",
1285-
ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
1284+
"title",
1285+
ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
12861286
)
12871287
);
12881288
movies = this.movieRepository.findAll(movieExample);
12891289
----
12901290

1291+
You can also negate individual properties. This will add an appropriate `NOT` operation, thus turning an `=` into a `<>`.
1292+
All scalar datatypes and all string operators are supported:
1293+
1294+
[source,java,indent=0,tabsize=4]
1295+
[[find-by-example-example-with-negated-properties]]
1296+
.findByExample with negated values
1297+
----
1298+
Example<MovieEntity> movieExample = Example.of(
1299+
new MovieEntity("Matrix", null),
1300+
ExampleMatcher
1301+
.matchingAny()
1302+
.withMatcher(
1303+
"title",
1304+
ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
1305+
)
1306+
.withTransformer("title", Neo4jPropertyValueTransformers.notMatching())
1307+
);
1308+
Flux<MovieEntity> allMoviesThatNotContainMatrix = this.movieRepository.findAll(movieExample);
1309+
----
1310+
12911311
[[faq.spring-boot.sdn]]
12921312
== Do I need Spring Boot to use Spring Data Neo4j?
12931313

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2011-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.core;
17+
18+
import org.springframework.data.domain.ExampleMatcher;
19+
20+
/**
21+
* Contains some useful transformers for adding additional, supported transformations to {@link ExampleMatcher example matchers} via
22+
* {@link org.springframework.data.domain.ExampleMatcher#withTransformer(String, ExampleMatcher.PropertyValueTransformer)}.
23+
*
24+
* @author Michael J. Simons
25+
* @since 6.3.11
26+
* @soundtrack Subway To Sally - Herzblut
27+
*/
28+
public abstract class Neo4jPropertyValueTransformers {
29+
30+
/**
31+
* A transformer that will indicate that the generated condition for the specific property shall be negated, creating
32+
* a {@code n.property != $property} for the equality operator for example.
33+
*
34+
* @return A value transformer negating values.
35+
*/
36+
public static ExampleMatcher.PropertyValueTransformer notMatching() {
37+
return o -> o.map(NegatedValue::new);
38+
}
39+
40+
/**
41+
* A wrapper indicating a negated value (will be used as {@code n.property != $parameter} (in case of string properties
42+
* all operators and not only the equality operator are supported, such as {@code not (n.property contains 'x')}.
43+
*
44+
* @param value The value used in the negated condition.
45+
*/
46+
public record NegatedValue(Object value) {
47+
}
48+
49+
private Neo4jPropertyValueTransformers() {
50+
}
51+
}

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

+21-14
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.neo4j.cypherdsl.core.StatementBuilder;
3434
import org.springframework.data.domain.Example;
3535
import org.springframework.data.domain.ExampleMatcher;
36+
import org.springframework.data.neo4j.core.Neo4jPropertyValueTransformers;
3637
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
3738
import org.springframework.data.neo4j.core.mapping.Constants;
3839
import org.springframework.data.neo4j.core.mapping.GraphPropertyDescription;
@@ -91,15 +92,17 @@ static <S> Predicate create(Neo4jMappingContext mappingContext, Example<S> examp
9192

9293
Neo4jConversionService conversionService = mappingContext.getConversionService();
9394

95+
var theValue = optionalValue.map(v -> v instanceof Neo4jPropertyValueTransformers.NegatedValue negatedValue ? negatedValue.value() : v).get();
96+
Condition condition = null;
97+
9498
if (graphProperty.isRelationship()) {
9599
Neo4jQuerySupport.REPOSITORY_QUERY_LOG.error("Querying by example does not support traversing of relationships.");
96100
} else if (graphProperty.isIdProperty() && probeNodeDescription.isUsingInternalIds()) {
97-
predicate.add(mode,
98-
predicate.neo4jPersistentEntity.getIdExpression().isEqualTo(literalOf(optionalValue.get())));
101+
condition = predicate.neo4jPersistentEntity.getIdExpression().isEqualTo(literalOf(theValue));
99102
} else {
100103
Expression property = property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(probeNodeDescription), propertyName);
101104
Expression parameter = parameter(propertyName);
102-
Condition condition = property.isEqualTo(parameter);
105+
condition = property.isEqualTo(parameter);
103106

104107
if (String.class.equals(graphProperty.getActualType())) {
105108

@@ -109,29 +112,33 @@ static <S> Predicate create(Neo4jMappingContext mappingContext, Example<S> examp
109112
}
110113

111114
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 DEFAULT, EXACT -> property.isEqualTo(parameter);
115116
case CONTAINING -> property.contains(parameter);
116117
case STARTING -> property.startsWith(parameter);
117118
case ENDING -> property.endsWith(parameter);
118119
case REGEX -> property.matches(parameter);
119120
};
120121
}
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());
122+
123+
Neo4jPersistentProperty neo4jPersistentProperty = (Neo4jPersistentProperty) graphProperty;
124+
predicate.parameters.put(propertyName, conversionService.writeValue(theValue,
125+
neo4jPersistentProperty.getTypeInformation(), neo4jPersistentProperty.getOptionalConverter()));
126+
}
127+
if (condition != null) {
128+
predicate.add(mode, postProcess(condition, optionalValue.get()));
129129
}
130130
}
131131

132132
return predicate;
133133
}
134134

135+
private static Condition postProcess(Condition condition, Object transformedValue) {
136+
if (transformedValue instanceof Neo4jPropertyValueTransformers.NegatedValue) {
137+
return condition.not();
138+
}
139+
return condition;
140+
}
141+
135142
private final Neo4jPersistentEntity neo4jPersistentEntity;
136143

137144
private Condition condition = Conditions.noCondition();

src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java

+40
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
import org.springframework.data.neo4j.core.DatabaseSelection;
8787
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
8888
import org.springframework.data.neo4j.core.Neo4jClient;
89+
import org.springframework.data.neo4j.core.Neo4jPropertyValueTransformers;
8990
import org.springframework.data.neo4j.core.Neo4jTemplate;
9091
import org.springframework.data.neo4j.core.UserSelection;
9192
import org.springframework.data.neo4j.core.UserSelectionProvider;
@@ -2968,6 +2969,45 @@ void countByExampleFluent(@Autowired PersonRepository repository) {
29682969
assertThat(count).isEqualTo(1);
29692970
}
29702971

2972+
@Test // GH-2703
2973+
void negatedProperties(@Autowired PersonRepository repository) {
2974+
2975+
var example = Example.of(new PersonWithAllConstructor(null, person1.getName(), null, null, null, null, null, null, null, null, null),
2976+
ExampleMatcher.matchingAll().withTransformer("name", Neo4jPropertyValueTransformers.notMatching()));
2977+
2978+
var optionalPerson = repository.findOne(example);
2979+
assertThat(optionalPerson)
2980+
.map(PersonWithAllConstructor::getName)
2981+
.hasValue(person2.getName());
2982+
}
2983+
2984+
@Test // GH-2703
2985+
void negatedInternalIdProperty(@Autowired PersonRepository repository) {
2986+
2987+
var example = Example.of(new PersonWithAllConstructor(person1.getId(), null, null, null, null, null, null, null, null, null, null),
2988+
ExampleMatcher.matchingAll().withTransformer("id", Neo4jPropertyValueTransformers.notMatching()));
2989+
2990+
var optionalPerson = repository.findOne(example);
2991+
assertThat(optionalPerson)
2992+
.map(PersonWithAllConstructor::getName)
2993+
.hasValue(person2.getName());
2994+
}
2995+
2996+
@Test // GH-2240
2997+
void negatedWithExternallyGeneratedId(@Autowired BidirectionalExternallyGeneratedIdRepository repository) {
2998+
2999+
BidirectionalExternallyGeneratedId a = repository.save(new BidirectionalExternallyGeneratedId());
3000+
BidirectionalExternallyGeneratedId b = repository.save(new BidirectionalExternallyGeneratedId());
3001+
3002+
var example = Example.of(a,
3003+
ExampleMatcher.matchingAll().withTransformer("uuid", Neo4jPropertyValueTransformers.notMatching()));
3004+
3005+
var optionalResult = repository.findOne(example);
3006+
assertThat(optionalResult)
3007+
.map(BidirectionalExternallyGeneratedId::getUuid)
3008+
.hasValue(b.getUuid());
3009+
}
3010+
29713011
@Test
29723012
void findEntityWithRelationshipByFindOneByExample(@Autowired RelationshipRepository repository) {
29733013

src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalExternallyGeneratedId.java

+7
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,11 @@ public class BidirectionalExternallyGeneratedId {
3535
@Relationship("OTHER")
3636
public BidirectionalExternallyGeneratedId otter;
3737

38+
public UUID getUuid() {
39+
return uuid;
40+
}
41+
42+
public BidirectionalExternallyGeneratedId getOtter() {
43+
return otter;
44+
}
3845
}

0 commit comments

Comments
 (0)