diff --git a/src/main/asciidoc/query-by-example.adoc b/src/main/asciidoc/query-by-example.adoc index ab63fd71f..384ec44ca 100644 --- a/src/main/asciidoc/query-by-example.adoc +++ b/src/main/asciidoc/query-by-example.adoc @@ -27,7 +27,6 @@ Query by Example is suited for several use-cases but also comes with limitations **Limitations** -* Query predicates are combined using the `AND` keyword * No support for nested/grouped property constraints like `firstname = ?0 or (firstname = ?1 and lastname = ?2)` * Only supports starts/contains/ends/regex matching for strings and exact matching for other property types @@ -107,13 +106,15 @@ Example example = Example.of(person, matcher); <7> ---- <1> Create a new instance of the domain object. <2> Set properties. -<3> Create an `ExampleMatcher` which is usable at this stage even without further configuration. +<3> Create an `ExampleMatcher` to expect all values to match. It's usable at this stage even without further configuration. <4> Construct a new `ExampleMatcher` to ignore the property path `lastname`. <5> Construct a new `ExampleMatcher` to ignore the property path `lastname` and to include null values. <6> Construct a new `ExampleMatcher` to ignore the property path `lastname`, to include null values, and use perform suffix string matching. <7> Create a new `Example` based on the domain object and the configured `ExampleMatcher`. ==== +By default the `ExampleMatcher` will expect all values set on the probe to match. If you want to get results matching any of the predicates defined implicitly, use `ExampleMatcher.matchingAny()`. + You can specify behavior for individual properties (e.g. "firstname" and "lastname", "address.city" for nested properties). You can tune it with matching options and case sensitivity. .Configuring matcher options @@ -164,4 +165,3 @@ Queries created by `Example` use a merged view of the configuration. Default mat | Property path |=== - diff --git a/src/main/java/org/springframework/data/domain/ExampleMatcher.java b/src/main/java/org/springframework/data/domain/ExampleMatcher.java index ecced45ee..18a62bcc1 100644 --- a/src/main/java/org/springframework/data/domain/ExampleMatcher.java +++ b/src/main/java/org/springframework/data/domain/ExampleMatcher.java @@ -20,6 +20,7 @@ import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.experimental.FieldDefaults; +import lombok.experimental.Wither; import java.util.Arrays; import java.util.Collection; @@ -43,6 +44,7 @@ import org.springframework.util.Assert; * * @author Christoph Strobl * @author Mark Paluch + * @author Oliver Gierke * @param * @since 1.12 */ @@ -57,19 +59,45 @@ public class ExampleMatcher { PropertySpecifiers propertySpecifiers; Set ignoredPaths; boolean defaultIgnoreCase; + @Wither(AccessLevel.PRIVATE) MatchMode mode; private ExampleMatcher() { - this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections. emptySet(), false); + this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.emptySet(), false, + MatchMode.ALL); } /** - * Create a new untyped {@link ExampleMatcher} including all non-null properties by default. + * Create a new {@link ExampleMatcher} including all non-null properties by default exposing that all resulting + * predicates are supposed to be AND-concatenated. * - * @param type must not be {@literal null}. + * @param type will never be {@literal null}. * @return + * @see #matchingAll() */ public static ExampleMatcher matching() { - return new ExampleMatcher(); + return matchingAll(); + } + + /** + * Create a new {@link ExampleMatcher} including all non-null properties by default matching any predicate derived + * from the example. + * + * @param type will never be {@literal null}. + * @return + */ + public static ExampleMatcher matchingAny() { + return new ExampleMatcher().withMode(MatchMode.ANY); + } + + /** + * Create a new {@link ExampleMatcher} including all non-null properties by default matching all predicates derived + * from the example. + * + * @param type will never be {@literal null}. + * @return + */ + public static ExampleMatcher matchingAll() { + return new ExampleMatcher().withMode(MatchMode.ALL); } /** @@ -87,8 +115,8 @@ public class ExampleMatcher { Set newIgnoredPaths = new LinkedHashSet(this.ignoredPaths); newIgnoredPaths.addAll(Arrays.asList(ignoredPaths)); - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, newIgnoredPaths, - defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, newIgnoredPaths, defaultIgnoreCase, + mode); } /** @@ -102,7 +130,8 @@ public class ExampleMatcher { Assert.notNull(ignoredPaths, "DefaultStringMatcher must not be empty!"); - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } /** @@ -123,7 +152,8 @@ public class ExampleMatcher { * @return */ public ExampleMatcher withIgnoreCase(boolean defaultIgnoreCase) { - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } /** @@ -175,7 +205,8 @@ public class ExampleMatcher { propertySpecifiers.add(propertySpecifier); - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } /** @@ -196,7 +227,8 @@ public class ExampleMatcher { propertySpecifiers.add(propertySpecifier.withValueTransformer(propertyValueTransformer)); - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } /** @@ -218,7 +250,8 @@ public class ExampleMatcher { propertySpecifiers.add(propertySpecifier.withIgnoreCase(true)); } - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } private PropertySpecifier getOrCreatePropertySpecifier(String propertyPath, PropertySpecifiers propertySpecifiers) { @@ -237,8 +270,7 @@ public class ExampleMatcher { * @return */ public ExampleMatcher withIncludeNullValues() { - return new ExampleMatcher(NullHandler.INCLUDE, defaultStringMatcher, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + return withNullHandler(NullHandler.INCLUDE); } /** @@ -248,8 +280,7 @@ public class ExampleMatcher { * @return */ public ExampleMatcher withIgnoreNullValues() { - return new ExampleMatcher(NullHandler.IGNORE, defaultStringMatcher, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + return withNullHandler(NullHandler.IGNORE); } /** @@ -262,7 +293,8 @@ public class ExampleMatcher { public ExampleMatcher withNullHandler(NullHandler nullHandler) { Assert.notNull(nullHandler, "NullHandler must not be null!"); - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } /** @@ -312,6 +344,26 @@ public class ExampleMatcher { return propertySpecifiers; } + /** + * Returns whether all of the predicates of the {@link Example} are supposed to match. If {@literal false} is + * returned, it's sufficient if any of the predicates derived from the {@link Example} match. + * + * @return whether all of the predicates of the {@link Example} are supposed to match or any of them is sufficient. + */ + public boolean isAllMatching() { + return mode.equals(MatchMode.ALL); + } + + /** + * Returns whether it's sufficient that any of the predicates of the {@link Example} match. If {@literal false} is + * returned, all predicates derived from the example need to match to produce results. + * + * @return whether it's sufficient that any of the predicates of the {@link Example} match or all need to match. + */ + public boolean isAnyMatching() { + return mode.equals(MatchMode.ANY); + } + /** * Null handling for creating criterion out of an {@link Example}. * @@ -780,4 +832,15 @@ public class ExampleMatcher { return propertySpecifiers.values(); } } + + /** + * The match modes to expose so that clients can find about how to concatenate the predicates. + * + * @author Oliver Gierke + * @since 1.13 + * @see ExampleMatcher#isAllMatching() + */ + private static enum MatchMode { + ALL, ANY; + } } diff --git a/src/test/java/org/springframework/data/domain/ExampleMatcherUnitTests.java b/src/test/java/org/springframework/data/domain/ExampleMatcherUnitTests.java index 62cbefbf8..76e64f6dd 100644 --- a/src/test/java/org/springframework/data/domain/ExampleMatcherUnitTests.java +++ b/src/test/java/org/springframework/data/domain/ExampleMatcherUnitTests.java @@ -29,6 +29,7 @@ import org.springframework.data.domain.ExampleMatcher.StringMatcher; * Unit test for {@link ExampleMatcher}. * * @author Mark Paluch + * @author Oliver Gierke * @soundtrack K2 - Der Berg Ruft (Club Mix) */ public class ExampleMatcherUnitTests { @@ -176,6 +177,36 @@ public class ExampleMatcherUnitTests { assertThat(configuredExampleSpec.isIgnoreCaseEnabled(), is(true)); } + /** + * @see DATACMNS-879 + */ + @Test + public void defaultMatcherRequiresAllMatching() { + + assertThat(matching().isAllMatching(), is(true)); + assertThat(matching().isAnyMatching(), is(false)); + } + + /** + * @see DATACMNS-879 + */ + @Test + public void allMatcherRequiresAllMatching() { + + assertThat(matchingAll().isAllMatching(), is(true)); + assertThat(matchingAll().isAnyMatching(), is(false)); + } + + /** + * @see DATACMNS-879 + */ + @Test + public void anyMatcherYieldsAnyMatching() { + + assertThat(matchingAny().isAnyMatching(), is(true)); + assertThat(matchingAny().isAllMatching(), is(false)); + } + static class Person { String firstname;