GH-2703 - Add support for negating transformers to query-by-example.

Closes #2703.
This commit is contained in:
Michael Simons
2023-03-30 12:14:48 +02:00
parent 3b1635537a
commit 02752f2bba
5 changed files with 143 additions and 21 deletions

View File

@@ -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?

View File

@@ -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() {
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -35,4 +35,11 @@ public class BidirectionalExternallyGeneratedId {
@Relationship("OTHER")
public BidirectionalExternallyGeneratedId otter;
public UUID getUuid() {
return uuid;
}
public BidirectionalExternallyGeneratedId getOtter() {
return otter;
}
}