Skip to content

Commit 2ce713f

Browse files
GH-2703 - Add support for negating transformers to query-by-example.
Closes #2703.
1 parent 8a7d2af commit 2ce713f

File tree

5 files changed

+174
-18
lines changed

5 files changed

+174
-18
lines changed

src/main/asciidoc/faq/index.adoc

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

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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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 java.util.Objects;
19+
20+
import org.springframework.data.domain.ExampleMatcher;
21+
22+
/**
23+
* Contains some useful transformers for adding additional, supported transformations to {@link ExampleMatcher example matchers} via
24+
* {@link org.springframework.data.domain.ExampleMatcher#withTransformer(String, ExampleMatcher.PropertyValueTransformer)}.
25+
*
26+
* @author Michael J. Simons
27+
* @since 6.3.11
28+
* @soundtrack Subway To Sally - Herzblut
29+
*/
30+
public abstract class Neo4jPropertyValueTransformers {
31+
32+
/**
33+
* A transformer that will indicate that the generated condition for the specific property shall be negated, creating
34+
* a {@code n.property != $property} for the equality operator for example.
35+
*
36+
* @return A value transformer negating values.
37+
*/
38+
public static ExampleMatcher.PropertyValueTransformer notMatching() {
39+
return o -> o.map(NegatedValue::new);
40+
}
41+
42+
/**
43+
* A wrapper indicating a negated value (will be used as {@code n.property != $parameter} (in case of string properties
44+
* all operators and not only the equality operator are supported, such as {@code not (n.property contains 'x')}.
45+
*/
46+
public static final class NegatedValue {
47+
48+
private final Object value;
49+
50+
/**
51+
* @param value The value used in the negated condition.
52+
*/
53+
public NegatedValue(Object value) {
54+
this.value = value;
55+
}
56+
57+
public Object value() {
58+
return value;
59+
}
60+
61+
@Override
62+
public boolean equals(Object obj) {
63+
if (obj == this) {
64+
return true;
65+
}
66+
if (obj == null || obj.getClass() != this.getClass()) {
67+
return false;
68+
}
69+
NegatedValue that = (NegatedValue) obj;
70+
return Objects.equals(this.value, that.value);
71+
}
72+
73+
@Override
74+
public int hashCode() {
75+
return Objects.hash(value);
76+
}
77+
}
78+
79+
private Neo4jPropertyValueTransformers() {
80+
}
81+
}

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

+21-13
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@
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.NegatedValue;
37+
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
3638
import org.springframework.data.neo4j.core.mapping.Constants;
3739
import org.springframework.data.neo4j.core.mapping.GraphPropertyDescription;
38-
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
3940
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
4041
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
4142
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
@@ -91,15 +92,17 @@ static <S> Predicate create(Neo4jMappingContext mappingContext, Example<S> examp
9192

9293
Neo4jConversionService conversionService = mappingContext.getConversionService();
9394

95+
Object theValue = optionalValue.map(v -> v instanceof NegatedValue ? ((NegatedValue) v).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

@@ -111,7 +114,6 @@ static <S> Predicate create(Neo4jMappingContext mappingContext, Example<S> examp
111114
switch (matcherAccessor.getStringMatcherForPath(currentPath)) {
112115
case DEFAULT:
113116
case EXACT:
114-
// This needs to be recreated as both property and parameter might have changed above
115117
condition = property.isEqualTo(parameter);
116118
break;
117119
case CONTAINING:
@@ -132,20 +134,26 @@ static <S> Predicate create(Neo4jMappingContext mappingContext, Example<S> examp
132134
.getStringMatcherForPath(currentPath));
133135
}
134136
}
135-
predicate.add(mode, condition);
136-
predicate.parameters.put(propertyName, optionalValue.map(
137-
v -> {
138-
Neo4jPersistentProperty neo4jPersistentProperty = (Neo4jPersistentProperty) graphProperty;
139-
return conversionService.writeValue(v, neo4jPersistentProperty.getTypeInformation(),
140-
neo4jPersistentProperty.getOptionalConverter());
141-
})
142-
.get());
137+
138+
Neo4jPersistentProperty neo4jPersistentProperty = (Neo4jPersistentProperty) graphProperty;
139+
predicate.parameters.put(propertyName, conversionService.writeValue(theValue,
140+
neo4jPersistentProperty.getTypeInformation(), neo4jPersistentProperty.getOptionalConverter()));
141+
}
142+
if (condition != null) {
143+
predicate.add(mode, postProcess(condition, optionalValue.get()));
143144
}
144145
}
145146

146147
return predicate;
147148
}
148149

150+
private static Condition postProcess(Condition condition, Object transformedValue) {
151+
if (transformedValue instanceof NegatedValue) {
152+
return condition.not();
153+
}
154+
return condition;
155+
}
156+
149157
private final Neo4jPersistentEntity neo4jPersistentEntity;
150158

151159
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
@@ -85,6 +85,7 @@
8585
import org.springframework.data.neo4j.core.DatabaseSelection;
8686
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
8787
import org.springframework.data.neo4j.core.Neo4jClient;
88+
import org.springframework.data.neo4j.core.Neo4jPropertyValueTransformers;
8889
import org.springframework.data.neo4j.core.Neo4jTemplate;
8990
import org.springframework.data.neo4j.core.UserSelection;
9091
import org.springframework.data.neo4j.core.UserSelectionProvider;
@@ -2935,6 +2936,45 @@ void countByExampleFluent(@Autowired PersonRepository repository) {
29352936
assertThat(count).isEqualTo(1);
29362937
}
29372938

2939+
@Test // GH-2703
2940+
void negatedProperties(@Autowired PersonRepository repository) {
2941+
2942+
Example<PersonWithAllConstructor> example = Example.of(new PersonWithAllConstructor(null, person1.getName(), null, null, null, null, null, null, null, null, null),
2943+
ExampleMatcher.matchingAll().withTransformer("name", Neo4jPropertyValueTransformers.notMatching()));
2944+
2945+
Optional<PersonWithAllConstructor> optionalPerson = repository.findOne(example);
2946+
assertThat(optionalPerson)
2947+
.map(PersonWithAllConstructor::getName)
2948+
.hasValue(person2.getName());
2949+
}
2950+
2951+
@Test // GH-2703
2952+
void negatedInternalIdProperty(@Autowired PersonRepository repository) {
2953+
2954+
Example<PersonWithAllConstructor> example = Example.of(new PersonWithAllConstructor(person1.getId(), null, null, null, null, null, null, null, null, null, null),
2955+
ExampleMatcher.matchingAll().withTransformer("id", Neo4jPropertyValueTransformers.notMatching()));
2956+
2957+
Optional<PersonWithAllConstructor> optionalPerson = repository.findOne(example);
2958+
assertThat(optionalPerson)
2959+
.map(PersonWithAllConstructor::getName)
2960+
.hasValue(person2.getName());
2961+
}
2962+
2963+
@Test // GH-2240
2964+
void negatedWithExternallyGeneratedId(@Autowired BidirectionalExternallyGeneratedIdRepository repository) {
2965+
2966+
BidirectionalExternallyGeneratedId a = repository.save(new BidirectionalExternallyGeneratedId());
2967+
BidirectionalExternallyGeneratedId b = repository.save(new BidirectionalExternallyGeneratedId());
2968+
2969+
Example<BidirectionalExternallyGeneratedId> example = Example.of(a,
2970+
ExampleMatcher.matchingAll().withTransformer("uuid", Neo4jPropertyValueTransformers.notMatching()));
2971+
2972+
Optional<BidirectionalExternallyGeneratedId> optionalResult = repository.findOne(example);
2973+
assertThat(optionalResult)
2974+
.map(BidirectionalExternallyGeneratedId::getUuid)
2975+
.hasValue(b.getUuid());
2976+
}
2977+
29382978
@Test
29392979
void findEntityWithRelationshipByFindOneByExample(@Autowired RelationshipRepository repository) {
29402980

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)