GH-2703 - Add support for negating transformers to query-by-example.
Closes #2703.
This commit is contained in:
@@ -1277,17 +1277,37 @@ Example<MovieEntity> movieExample = Example.of(new MovieEntity("The Matrix", nul
|
||||
Flux<MovieEntity> movies = this.movieRepository.findAll(movieExample);
|
||||
|
||||
movieExample = Example.of(
|
||||
new MovieEntity("Matrix", null),
|
||||
ExampleMatcher
|
||||
.matchingAny()
|
||||
new MovieEntity("Matrix", null),
|
||||
ExampleMatcher
|
||||
.matchingAny()
|
||||
.withMatcher(
|
||||
"title",
|
||||
ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
|
||||
"title",
|
||||
ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
|
||||
)
|
||||
);
|
||||
movies = this.movieRepository.findAll(movieExample);
|
||||
----
|
||||
|
||||
You can also negate individual properties. This will add an appropriate `NOT` operation, thus turning an `=` into a `<>`.
|
||||
All scalar datatypes and all string operators are supported:
|
||||
|
||||
[source,java,indent=0,tabsize=4]
|
||||
[[find-by-example-example-with-negated-properties]]
|
||||
.findByExample with negated values
|
||||
----
|
||||
Example<MovieEntity> movieExample = Example.of(
|
||||
new MovieEntity("Matrix", null),
|
||||
ExampleMatcher
|
||||
.matchingAny()
|
||||
.withMatcher(
|
||||
"title",
|
||||
ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
|
||||
)
|
||||
.withTransformer("title", Neo4jPropertyValueTransformers.notMatching())
|
||||
);
|
||||
Flux<MovieEntity> allMoviesThatNotContainMatrix = this.movieRepository.findAll(movieExample);
|
||||
----
|
||||
|
||||
[[faq.spring-boot.sdn]]
|
||||
== Do I need Spring Boot to use Spring Data Neo4j?
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.core;
|
||||
|
||||
import org.springframework.data.domain.ExampleMatcher;
|
||||
|
||||
/**
|
||||
* Contains some useful transformers for adding additional, supported transformations to {@link ExampleMatcher example matchers} via
|
||||
* {@link org.springframework.data.domain.ExampleMatcher#withTransformer(String, ExampleMatcher.PropertyValueTransformer)}.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 6.3.11
|
||||
* @soundtrack Subway To Sally - Herzblut
|
||||
*/
|
||||
public abstract class Neo4jPropertyValueTransformers {
|
||||
|
||||
/**
|
||||
* A transformer that will indicate that the generated condition for the specific property shall be negated, creating
|
||||
* a {@code n.property != $property} for the equality operator for example.
|
||||
*
|
||||
* @return A value transformer negating values.
|
||||
*/
|
||||
public static ExampleMatcher.PropertyValueTransformer notMatching() {
|
||||
return o -> o.map(NegatedValue::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper indicating a negated value (will be used as {@code n.property != $parameter} (in case of string properties
|
||||
* all operators and not only the equality operator are supported, such as {@code not (n.property contains 'x')}.
|
||||
*
|
||||
* @param value The value used in the negated condition.
|
||||
*/
|
||||
public record NegatedValue(Object value) {
|
||||
}
|
||||
|
||||
private Neo4jPropertyValueTransformers() {
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ 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.Neo4jPropertyValueTransformers;
|
||||
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
|
||||
import org.springframework.data.neo4j.core.mapping.Constants;
|
||||
import org.springframework.data.neo4j.core.mapping.GraphPropertyDescription;
|
||||
@@ -159,18 +160,19 @@ final class Predicate {
|
||||
Neo4jConversionService conversionService = mappingContext.getConversionService();
|
||||
boolean isRootNode = predicate.neo4jPersistentEntity.equals(nodeDescription);
|
||||
|
||||
var theValue = optionalValue.map(v -> v instanceof Neo4jPropertyValueTransformers.NegatedValue negatedValue ? negatedValue.value() : v).get();
|
||||
Condition condition;
|
||||
|
||||
if (graphProperty.isIdProperty() && nodeDescription.isUsingInternalIds()) {
|
||||
if (isRootNode) {
|
||||
predicate.add(mode,
|
||||
predicate.neo4jPersistentEntity.getIdExpression().isEqualTo(literalOf(optionalValue.get())));
|
||||
condition = predicate.neo4jPersistentEntity.getIdExpression().isEqualTo(literalOf(theValue));
|
||||
} else {
|
||||
predicate.add(mode,
|
||||
nodeDescription.getIdExpression().isEqualTo(literalOf(optionalValue.get())));
|
||||
condition = nodeDescription.getIdExpression().isEqualTo(literalOf(theValue));
|
||||
}
|
||||
} 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);
|
||||
condition = property.isEqualTo(parameter);
|
||||
|
||||
if (String.class.equals(graphProperty.getActualType())) {
|
||||
|
||||
@@ -180,24 +182,26 @@ final class Predicate {
|
||||
}
|
||||
|
||||
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 DEFAULT, EXACT -> 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());
|
||||
|
||||
Neo4jPersistentProperty neo4jPersistentProperty = (Neo4jPersistentProperty) graphProperty;
|
||||
predicate.parameters.put(wrapper.getNodeName() + propertyName, conversionService.writeValue(theValue,
|
||||
neo4jPersistentProperty.getTypeInformation(), neo4jPersistentProperty.getOptionalConverter()));
|
||||
}
|
||||
predicate.add(mode, postProcess(condition, optionalValue.get()));
|
||||
}
|
||||
|
||||
private static Condition postProcess(Condition condition, Object transformedValue) {
|
||||
if (transformedValue instanceof Neo4jPropertyValueTransformers.NegatedValue) {
|
||||
return condition.not();
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
|
||||
private final Neo4jPersistentEntity neo4jPersistentEntity;
|
||||
|
||||
@@ -91,6 +91,7 @@ import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.neo4j.core.DatabaseSelection;
|
||||
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.core.Neo4jPropertyValueTransformers;
|
||||
import org.springframework.data.neo4j.core.Neo4jTemplate;
|
||||
import org.springframework.data.neo4j.core.UserSelection;
|
||||
import org.springframework.data.neo4j.core.UserSelectionProvider;
|
||||
@@ -2996,6 +2997,45 @@ class RepositoryIT {
|
||||
assertThat(count).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test // GH-2703
|
||||
void negatedProperties(@Autowired PersonRepository repository) {
|
||||
|
||||
var example = Example.of(new PersonWithAllConstructor(null, person1.getName(), null, null, null, null, null, null, null, null, null),
|
||||
ExampleMatcher.matchingAll().withTransformer("name", Neo4jPropertyValueTransformers.notMatching()));
|
||||
|
||||
var optionalPerson = repository.findOne(example);
|
||||
assertThat(optionalPerson)
|
||||
.map(PersonWithAllConstructor::getName)
|
||||
.hasValue(person2.getName());
|
||||
}
|
||||
|
||||
@Test // GH-2703
|
||||
void negatedInternalIdProperty(@Autowired PersonRepository repository) {
|
||||
|
||||
var example = Example.of(new PersonWithAllConstructor(person1.getId(), null, null, null, null, null, null, null, null, null, null),
|
||||
ExampleMatcher.matchingAll().withTransformer("id", Neo4jPropertyValueTransformers.notMatching()));
|
||||
|
||||
var optionalPerson = repository.findOne(example);
|
||||
assertThat(optionalPerson)
|
||||
.map(PersonWithAllConstructor::getName)
|
||||
.hasValue(person2.getName());
|
||||
}
|
||||
|
||||
@Test // GH-2240
|
||||
void negatedWithExternallyGeneratedId(@Autowired BidirectionalExternallyGeneratedIdRepository repository) {
|
||||
|
||||
BidirectionalExternallyGeneratedId a = repository.save(new BidirectionalExternallyGeneratedId());
|
||||
BidirectionalExternallyGeneratedId b = repository.save(new BidirectionalExternallyGeneratedId());
|
||||
|
||||
var example = Example.of(a,
|
||||
ExampleMatcher.matchingAll().withTransformer("uuid", Neo4jPropertyValueTransformers.notMatching()));
|
||||
|
||||
var optionalResult = repository.findOne(example);
|
||||
assertThat(optionalResult)
|
||||
.map(BidirectionalExternallyGeneratedId::getUuid)
|
||||
.hasValue(b.getUuid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findEntityWithRelationshipByFindOneByExample(@Autowired RelationshipRepository repository) {
|
||||
|
||||
|
||||
@@ -35,4 +35,11 @@ public class BidirectionalExternallyGeneratedId {
|
||||
@Relationship("OTHER")
|
||||
public BidirectionalExternallyGeneratedId otter;
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public BidirectionalExternallyGeneratedId getOtter() {
|
||||
return otter;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user