diff --git a/src/main/asciidoc/faq/index.adoc b/src/main/asciidoc/faq/index.adoc index 0817e49b7..ccd4b2dd6 100644 --- a/src/main/asciidoc/faq/index.adoc +++ b/src/main/asciidoc/faq/index.adoc @@ -1277,17 +1277,37 @@ Example movieExample = Example.of(new MovieEntity("The Matrix", nul Flux 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 movieExample = Example.of( + new MovieEntity("Matrix", null), + ExampleMatcher + .matchingAny() + .withMatcher( + "title", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING) + ) + .withTransformer("title", Neo4jPropertyValueTransformers.notMatching()) +); +Flux allMoviesThatNotContainMatrix = this.movieRepository.findAll(movieExample); +---- + [[faq.spring-boot.sdn]] == Do I need Spring Boot to use Spring Data Neo4j? diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jPropertyValueTransformers.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jPropertyValueTransformers.java new file mode 100644 index 000000000..ebf1c1efc --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jPropertyValueTransformers.java @@ -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() { + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java index 4f7454ddf..09432790f 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Predicate.java @@ -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; diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java index 38a1a7e47..1aeeabdea 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java @@ -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) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalExternallyGeneratedId.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalExternallyGeneratedId.java index 117181686..226f56a96 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalExternallyGeneratedId.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/BidirectionalExternallyGeneratedId.java @@ -35,4 +35,11 @@ public class BidirectionalExternallyGeneratedId { @Relationship("OTHER") public BidirectionalExternallyGeneratedId otter; + public UUID getUuid() { + return uuid; + } + + public BidirectionalExternallyGeneratedId getOtter() { + return otter; + } }