Skip to content

Commit 02752f2

Browse files
GH-2703 - Add support for negating transformers to query-by-example.
Closes #2703.
1 parent 3b16355 commit 02752f2

File tree

5 files changed

+143
-21
lines changed

5 files changed

+143
-21
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

+20-16
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.data.domain.Example;
3838
import org.springframework.data.domain.ExampleMatcher;
3939
import org.springframework.data.mapping.PropertyPath;
40+
import org.springframework.data.neo4j.core.Neo4jPropertyValueTransformers;
4041
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
4142
import org.springframework.data.neo4j.core.mapping.Constants;
4243
import org.springframework.data.neo4j.core.mapping.GraphPropertyDescription;
@@ -159,18 +160,19 @@ private static void addConditionAndParameters(Neo4jMappingContext mappingContext
159160
Neo4jConversionService conversionService = mappingContext.getConversionService();
160161
boolean isRootNode = predicate.neo4jPersistentEntity.equals(nodeDescription);
161162

163+
var theValue = optionalValue.map(v -> v instanceof Neo4jPropertyValueTransformers.NegatedValue negatedValue ? negatedValue.value() : v).get();
164+
Condition condition;
165+
162166
if (graphProperty.isIdProperty() && nodeDescription.isUsingInternalIds()) {
163167
if (isRootNode) {
164-
predicate.add(mode,
165-
predicate.neo4jPersistentEntity.getIdExpression().isEqualTo(literalOf(optionalValue.get())));
168+
condition = predicate.neo4jPersistentEntity.getIdExpression().isEqualTo(literalOf(theValue));
166169
} else {
167-
predicate.add(mode,
168-
nodeDescription.getIdExpression().isEqualTo(literalOf(optionalValue.get())));
170+
condition = nodeDescription.getIdExpression().isEqualTo(literalOf(theValue));
169171
}
170172
} else {
171173
Expression property = !isRootNode ? property(wrapper.getNodeName(), propertyName) : property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription), propertyName);
172174
Expression parameter = parameter(wrapper.getNodeName() + propertyName);
173-
Condition condition = property.isEqualTo(parameter);
175+
condition = property.isEqualTo(parameter);
174176

175177
if (String.class.equals(graphProperty.getActualType())) {
176178

@@ -180,24 +182,26 @@ private static void addConditionAndParameters(Neo4jMappingContext mappingContext
180182
}
181183

182184
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);
185+
case DEFAULT, EXACT -> property.isEqualTo(parameter);
186186
case CONTAINING -> property.contains(parameter);
187187
case STARTING -> property.startsWith(parameter);
188188
case ENDING -> property.endsWith(parameter);
189189
case REGEX -> property.matches(parameter);
190190
};
191191
}
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());
192+
193+
Neo4jPersistentProperty neo4jPersistentProperty = (Neo4jPersistentProperty) graphProperty;
194+
predicate.parameters.put(wrapper.getNodeName() + propertyName, conversionService.writeValue(theValue,
195+
neo4jPersistentProperty.getTypeInformation(), neo4jPersistentProperty.getOptionalConverter()));
196+
}
197+
predicate.add(mode, postProcess(condition, optionalValue.get()));
198+
}
199+
200+
private static Condition postProcess(Condition condition, Object transformedValue) {
201+
if (transformedValue instanceof Neo4jPropertyValueTransformers.NegatedValue) {
202+
return condition.not();
200203
}
204+
return condition;
201205
}
202206

203207
private final Neo4jPersistentEntity neo4jPersistentEntity;

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

+40
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
import org.springframework.data.neo4j.core.DatabaseSelection;
9292
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
9393
import org.springframework.data.neo4j.core.Neo4jClient;
94+
import org.springframework.data.neo4j.core.Neo4jPropertyValueTransformers;
9495
import org.springframework.data.neo4j.core.Neo4jTemplate;
9596
import org.springframework.data.neo4j.core.UserSelection;
9697
import org.springframework.data.neo4j.core.UserSelectionProvider;
@@ -2996,6 +2997,45 @@ void countByExampleFluent(@Autowired PersonRepository repository) {
29962997
assertThat(count).isEqualTo(1);
29972998
}
29982999

3000+
@Test // GH-2703
3001+
void negatedProperties(@Autowired PersonRepository repository) {
3002+
3003+
var example = Example.of(new PersonWithAllConstructor(null, person1.getName(), null, null, null, null, null, null, null, null, null),
3004+
ExampleMatcher.matchingAll().withTransformer("name", Neo4jPropertyValueTransformers.notMatching()));
3005+
3006+
var optionalPerson = repository.findOne(example);
3007+
assertThat(optionalPerson)
3008+
.map(PersonWithAllConstructor::getName)
3009+
.hasValue(person2.getName());
3010+
}
3011+
3012+
@Test // GH-2703
3013+
void negatedInternalIdProperty(@Autowired PersonRepository repository) {
3014+
3015+
var example = Example.of(new PersonWithAllConstructor(person1.getId(), null, null, null, null, null, null, null, null, null, null),
3016+
ExampleMatcher.matchingAll().withTransformer("id", Neo4jPropertyValueTransformers.notMatching()));
3017+
3018+
var optionalPerson = repository.findOne(example);
3019+
assertThat(optionalPerson)
3020+
.map(PersonWithAllConstructor::getName)
3021+
.hasValue(person2.getName());
3022+
}
3023+
3024+
@Test // GH-2240
3025+
void negatedWithExternallyGeneratedId(@Autowired BidirectionalExternallyGeneratedIdRepository repository) {
3026+
3027+
BidirectionalExternallyGeneratedId a = repository.save(new BidirectionalExternallyGeneratedId());
3028+
BidirectionalExternallyGeneratedId b = repository.save(new BidirectionalExternallyGeneratedId());
3029+
3030+
var example = Example.of(a,
3031+
ExampleMatcher.matchingAll().withTransformer("uuid", Neo4jPropertyValueTransformers.notMatching()));
3032+
3033+
var optionalResult = repository.findOne(example);
3034+
assertThat(optionalResult)
3035+
.map(BidirectionalExternallyGeneratedId::getUuid)
3036+
.hasValue(b.getUuid());
3037+
}
3038+
29993039
@Test
30003040
void findEntityWithRelationshipByFindOneByExample(@Autowired RelationshipRepository repository) {
30013041

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)