Browse Source

DATAMONGO-1768 - Introduce UntypedExampleMatcher.

Introducing UntypedExampleMatcher allows creation of QBE criteria that does not infer a strict type match.

Original pull request: #496.
pull/691/head
Christoph Strobl 8 years ago committed by Mark Paluch
parent
commit
2d825bed41
  1. 14
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java
  2. 234
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java
  3. 16
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryByExampleTests.java
  4. 14
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java
  5. 184
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcherUnitTests.java
  6. 26
      src/main/asciidoc/reference/query-by-example.adoc

14
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java

@ -29,7 +29,6 @@ import java.util.regex.Pattern; @@ -29,7 +29,6 @@ import java.util.regex.Pattern;
import org.bson.Document;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.ExampleMatcher.NullHandler;
import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer;
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
@ -40,6 +39,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -40,6 +39,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.MongoRegexCreator;
import org.springframework.data.mongodb.core.query.MongoRegexCreator.MatchMode;
import org.springframework.data.mongodb.core.query.SerializationUtils;
import org.springframework.data.mongodb.core.query.UntypedExampleMatcher;
import org.springframework.data.support.ExampleMatcherAccessor;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
@ -293,7 +293,7 @@ public class MongoExampleMapper { @@ -293,7 +293,7 @@ public class MongoExampleMapper {
Document result = new Document();
if (isTypeRestricting(example.getMatcher())) {
if (isTypeRestricting(example)) {
result.putAll(query);
this.converter.getTypeMapper().writeTypeRestrictions(result, getTypesToMatch(example));
@ -309,13 +309,17 @@ public class MongoExampleMapper { @@ -309,13 +309,17 @@ public class MongoExampleMapper {
return result;
}
private boolean isTypeRestricting(ExampleMatcher matcher) {
private boolean isTypeRestricting(Example example) {
if (matcher.getIgnoredPaths().isEmpty()) {
if (example.getMatcher() instanceof UntypedExampleMatcher) {
return false;
}
if (example.getMatcher().getIgnoredPaths().isEmpty()) {
return true;
}
for (String path : matcher.getIgnoredPaths()) {
for (String path : example.getMatcher().getIgnoredPaths()) {
if (this.converter.getTypeMapper().isTypeKey(path)) {
return false;
}

234
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java

@ -0,0 +1,234 @@ @@ -0,0 +1,234 @@
/*
* Copyright 2017 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
*
* http://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.mongodb.core.query;
import lombok.EqualsAndHashCode;
import java.util.Set;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.util.Assert;
/**
* {@link ExampleMatcher} implementation for query by example (QBE). Unlike plain {@link ExampleMatcher} this untyped
* counterpart does not enforce a strict type match when executing the query. This allows to use totally unrelated
* example documents as references for querying collections as long as the used field/property names match.
*
* @author Christoph Strobl
* @since 2.0
*/
@EqualsAndHashCode
public class UntypedExampleMatcher implements ExampleMatcher {
private final ExampleMatcher delegate;
/**
* Creates new {@link UntypedExampleMatcher}.
*
* @param delegate must not be {@literal null}.
*/
private UntypedExampleMatcher(ExampleMatcher delegate) {
Assert.notNull(delegate, "Delegate must not be null!");
this.delegate = delegate;
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#matching()
*/
public static UntypedExampleMatcher matching() {
return new UntypedExampleMatcher(ExampleMatcher.matching());
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#matchingAny()
*/
public static UntypedExampleMatcher matchingAny() {
return new UntypedExampleMatcher(ExampleMatcher.matchingAny());
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#matchingAll()
*/
public static UntypedExampleMatcher matchingAll() {
return new UntypedExampleMatcher(ExampleMatcher.matchingAll());
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#withIgnorePaths(java.lang.String...)
*/
public UntypedExampleMatcher withIgnorePaths(String... ignoredPaths) {
return new UntypedExampleMatcher(delegate.withIgnorePaths(ignoredPaths));
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#withStringMatcher(java.lang.String)
*/
public UntypedExampleMatcher withStringMatcher(StringMatcher defaultStringMatcher) {
return new UntypedExampleMatcher(delegate.withStringMatcher(defaultStringMatcher));
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#withIgnoreCase()
*/
public UntypedExampleMatcher withIgnoreCase() {
return new UntypedExampleMatcher(delegate.withIgnoreCase());
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#withIgnoreCase(boolean)
*/
public UntypedExampleMatcher withIgnoreCase(boolean defaultIgnoreCase) {
return new UntypedExampleMatcher(delegate.withIgnoreCase(defaultIgnoreCase));
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#withMatcher(java.lang.String, org.springframework.data.domain.ExampleMatcher.MatcherConfigurer)
*/
public UntypedExampleMatcher withMatcher(String propertyPath,
MatcherConfigurer<GenericPropertyMatcher> matcherConfigurer) {
return new UntypedExampleMatcher(delegate.withMatcher(propertyPath, matcherConfigurer));
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#withMatcher(java.lang.String, org.springframework.data.domain.ExampleMatcher.GenericPropertyMatcher)
*/
public UntypedExampleMatcher withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher) {
return new UntypedExampleMatcher(delegate.withMatcher(propertyPath, genericPropertyMatcher));
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#withTransformer(java.lang.String, org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer)
*/
public UntypedExampleMatcher withTransformer(String propertyPath, PropertyValueTransformer propertyValueTransformer) {
return new UntypedExampleMatcher(delegate.withTransformer(propertyPath, propertyValueTransformer));
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#withIgnoreCase(java.lang.String...)
*/
public UntypedExampleMatcher withIgnoreCase(String... propertyPaths) {
return new UntypedExampleMatcher(delegate.withIgnoreCase(propertyPaths));
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#withIncludeNullValues()
*/
public UntypedExampleMatcher withIncludeNullValues() {
return new UntypedExampleMatcher(delegate.withIncludeNullValues());
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#withIgnoreNullValues()
*/
public UntypedExampleMatcher withIgnoreNullValues() {
return new UntypedExampleMatcher(delegate.withIgnoreNullValues());
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#withNullHandler(org.springframework.data.domain.ExampleMatcher.NullHandler)
*/
public UntypedExampleMatcher withNullHandler(NullHandler nullHandler) {
return new UntypedExampleMatcher(delegate.withNullHandler(nullHandler));
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#getNullHandler()
*/
public NullHandler getNullHandler() {
return delegate.getNullHandler();
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#getDefaultStringMatcher()
*/
public StringMatcher getDefaultStringMatcher() {
return delegate.getDefaultStringMatcher();
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#isIgnoreCaseEnabled()
*/
public boolean isIgnoreCaseEnabled() {
return delegate.isIgnoreCaseEnabled();
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#isIgnoredPath()
*/
public boolean isIgnoredPath(String path) {
return delegate.isIgnoredPath(path);
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#getIgnoredPaths()
*/
public Set<String> getIgnoredPaths() {
return delegate.getIgnoredPaths();
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#getPropertySpecifiers()
*/
public PropertySpecifiers getPropertySpecifiers() {
return delegate.getPropertySpecifiers();
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#isAllMatching()
*/
public boolean isAllMatching() {
return delegate.isAllMatching();
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#isAnyMatching()
*/
public boolean isAnyMatching() {
return delegate.isAnyMatching();
}
/*
* (non-Javadoc)
* @see org.springframework.data.domain.ExampleMatcher#getMatchMode()
*/
public MatchMode getMatchMode() {
return delegate.getMatchMode();
}
}

16
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryByExampleTests.java

@ -33,6 +33,7 @@ import org.springframework.data.mongodb.core.mapping.Document; @@ -33,6 +33,7 @@ import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.UntypedExampleMatcher;
import com.mongodb.MongoClient;
@ -179,7 +180,7 @@ public class QueryByExampleTests { @@ -179,7 +180,7 @@ public class QueryByExampleTests {
}
@Test // DATAMONGO-1768
public void untypedExampleMatchesCorrectly() {
public void exampleIgnoringClassTypeKeyMatchesCorrectly() {
NotAPersonButStillMatchingFields probe = new NotAPersonButStillMatchingFields();
probe.lastname = "stark";
@ -192,6 +193,19 @@ public class QueryByExampleTests { @@ -192,6 +193,19 @@ public class QueryByExampleTests {
assertThat(result, hasItems(p1, p3));
}
@Test // DATAMONGO-1768
public void untypedExampleMatchesCorrectly() {
NotAPersonButStillMatchingFields probe = new NotAPersonButStillMatchingFields();
probe.lastname = "stark";
Query query = new Query(new Criteria().alike(Example.of(probe, UntypedExampleMatcher.matching())));
List<Person> result = operations.find(query, Person.class);
assertThat(result, hasSize(2));
assertThat(result, hasItems(p1, p3));
}
@Document(collection = "dramatis-personae")
@EqualsAndHashCode
@ToString

14
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java

@ -46,6 +46,7 @@ import org.springframework.data.mongodb.core.mapping.DBRef; @@ -46,6 +46,7 @@ import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.UntypedExampleMatcher;
import org.springframework.data.mongodb.test.util.IsBsonObject;
import org.springframework.data.util.TypeInformation;
@ -470,7 +471,7 @@ public class MongoExampleMapperUnitTests { @@ -470,7 +471,7 @@ public class MongoExampleMapperUnitTests {
@Override
public void writeType(TypeInformation<?> info, Bson sink) {
((org.bson.Document) sink).put("_foo", "bar");
((org.bson.Document) sink).put("_foo", "bar");
}
});
@ -482,6 +483,17 @@ public class MongoExampleMapperUnitTests { @@ -482,6 +483,17 @@ public class MongoExampleMapperUnitTests {
assertThat(document, isBsonObject().notContaining("_class").notContaining("_foo"));
}
@Test // DATAMONGO-1768
public void untypedExampleShouldNotInfereTypeRestriction() {
WrapperDocument probe = new WrapperDocument();
probe.flatDoc = new FlatDocument();
probe.flatDoc.stringValue = "conflux";
org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching()));
assertThat(document, isBsonObject().notContaining("_class"));
}
static class FlatDocument {
@Id String id;

184
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcherUnitTests.java

@ -0,0 +1,184 @@ @@ -0,0 +1,184 @@
/*
* Copyright 2017 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
*
* http://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.mongodb.core.query;
import static org.assertj.core.api.Assertions.*;
import static org.hamcrest.Matchers.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.ExampleMatcher.NullHandler;
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
/**
* @author Christoph Strobl
*/
public class UntypedExampleMatcherUnitTests {
ExampleMatcher matcher;
@Before
public void setUp() throws Exception {
matcher = UntypedExampleMatcher.matching();
}
@Test // DATAMONGO-1768
public void defaultStringMatcherShouldReturnDefault() {
assertThat(matcher.getDefaultStringMatcher()).isEqualTo(StringMatcher.DEFAULT);
}
@Test // DATAMONGO-1768
public void ignoreCaseShouldReturnFalseByDefault() {
assertThat(matcher.isIgnoreCaseEnabled()).isFalse();
}
@Test // DATAMONGO-1768
public void ignoredPathsIsEmptyByDefault() {
assertThat(matcher.getIgnoredPaths()).isEmpty();
}
@Test // DATAMONGO-1768
public void nullHandlerShouldReturnIgnoreByDefault() {
assertThat(matcher.getNullHandler()).isEqualTo(NullHandler.IGNORE);
}
@Test(expected = UnsupportedOperationException.class) // DATAMONGO-1768
public void ignoredPathsIsNotModifiable() throws Exception {
matcher.getIgnoredPaths().add("¯\\_(ツ)_/¯");
}
@Test // DATAMONGO-1768
public void ignoreCaseShouldReturnTrueWhenIgnoreCaseEnabled() {
matcher = UntypedExampleMatcher.matching().withIgnoreCase();
assertThat(matcher.isIgnoreCaseEnabled()).isTrue();
}
@Test // DATAMONGO-1768
public void ignoreCaseShouldReturnTrueWhenIgnoreCaseSet() {
matcher = UntypedExampleMatcher.matching().withIgnoreCase(true);
assertThat(matcher.isIgnoreCaseEnabled()).isTrue();
}
@Test // DATAMONGO-1768
public void nullHandlerShouldReturnInclude() throws Exception {
matcher = UntypedExampleMatcher.matching().withIncludeNullValues();
assertThat(matcher.getNullHandler()).isEqualTo(NullHandler.INCLUDE);
}
@Test // DATAMONGO-1768
public void nullHandlerShouldReturnIgnore() {
matcher = UntypedExampleMatcher.matching().withIgnoreNullValues();
assertThat(matcher.getNullHandler()).isEqualTo(NullHandler.IGNORE);
}
@Test // DATAMONGO-1768
public void nullHandlerShouldReturnConfiguredValue() {
matcher = UntypedExampleMatcher.matching().withNullHandler(NullHandler.INCLUDE);
assertThat(matcher.getNullHandler()).isEqualTo(NullHandler.INCLUDE);
}
@Test // DATAMONGO-1768
public void ignoredPathsShouldReturnCorrectProperties() {
matcher = UntypedExampleMatcher.matching().withIgnorePaths("foo", "bar", "baz");
assertThat(matcher.getIgnoredPaths()).contains("foo", "bar", "baz");
assertThat(matcher.getIgnoredPaths()).hasSize(3);
}
@Test // DATAMONGO-1768
public void ignoredPathsShouldReturnUniqueProperties() {
matcher = UntypedExampleMatcher.matching().withIgnorePaths("foo", "bar", "foo");
assertThat(matcher.getIgnoredPaths()).contains("foo", "bar");
assertThat(matcher.getIgnoredPaths()).hasSize(2);
}
@Test // DATAMONGO-1768
public void withCreatesNewInstance() {
matcher = UntypedExampleMatcher.matching().withIgnorePaths("foo", "bar", "foo");
ExampleMatcher configuredExampleSpec = matcher.withIgnoreCase();
assertThat(matcher).isNotEqualTo(sameInstance(configuredExampleSpec));
assertThat(matcher.getIgnoredPaths()).hasSize(2);
assertThat(matcher.isIgnoreCaseEnabled()).isFalse();
assertThat(configuredExampleSpec.getIgnoredPaths()).hasSize(2);
assertThat(configuredExampleSpec.isIgnoreCaseEnabled()).isTrue();
}
@Test // DATAMONGO-1768
public void defaultMatcherRequiresAllMatching() {
assertThat(UntypedExampleMatcher.matching().isAllMatching()).isTrue();
assertThat(UntypedExampleMatcher.matching().isAnyMatching()).isFalse();
}
@Test // DATAMONGO-1768
public void allMatcherRequiresAllMatching() {
assertThat(UntypedExampleMatcher.matchingAll().isAllMatching()).isTrue();
assertThat(UntypedExampleMatcher.matchingAll().isAnyMatching()).isFalse();
}
@Test // DATAMONGO-1768
public void anyMatcherYieldsAnyMatching() {
assertThat(UntypedExampleMatcher.matchingAny().isAnyMatching()).isTrue();
assertThat(UntypedExampleMatcher.matchingAny().isAllMatching()).isFalse();
}
@Test // DATAMONGO-1768
public void shouldCompareUsingHashCodeAndEquals() {
matcher = UntypedExampleMatcher.matching() //
.withIgnorePaths("foo", "bar", "baz") //
.withNullHandler(NullHandler.IGNORE) //
.withIgnoreCase("ignored-case") //
.withMatcher("hello", ExampleMatcher.GenericPropertyMatchers.contains().caseSensitive()) //
.withMatcher("world", matcher -> matcher.endsWith());
ExampleMatcher sameAsMatcher = UntypedExampleMatcher.matching() //
.withIgnorePaths("foo", "bar", "baz") //
.withNullHandler(NullHandler.IGNORE) //
.withIgnoreCase("ignored-case") //
.withMatcher("hello", ExampleMatcher.GenericPropertyMatchers.contains().caseSensitive()) //
.withMatcher("world", matcher -> matcher.endsWith());
ExampleMatcher different = UntypedExampleMatcher.matching() //
.withIgnorePaths("foo", "bar", "baz") //
.withNullHandler(NullHandler.IGNORE) //
.withMatcher("hello", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase());
assertThat(matcher.hashCode()).isEqualTo(sameAsMatcher.hashCode());
assertThat(matcher.hashCode()).isNotEqualTo(different.hashCode());
assertThat(matcher).isEqualTo(sameAsMatcher);
assertThat(matcher).isNotEqualTo(different);
}
}

26
src/main/asciidoc/reference/query-by-example.adoc

@ -69,3 +69,29 @@ Spring Data MongoDB provides support for the following matching options: @@ -69,3 +69,29 @@ Spring Data MongoDB provides support for the following matching options:
| `{"firstname" : { $regex: /firstname/, $options: 'i'}}`
|===
[[query-by-example.untyped]]
== Untyped Example
By default `Example` is strictly typed. This means the mapped query will have a type match included restricting it to probe assignable types. Eg. when sticking with the default type key `_class` the query has restrictions like `_class : { $in : [ com.acme.Person] }`.
By using the `UntypedExampleMatcher` it is possible bypasses the default behavior and skip the type restriction. So as long as field names match nearly any domain type can be used as the probe for creating the reference.
.Untyped Example Query
====
[source, java]
----
class JustAnArbitraryClassWithMatchingFieldName {
@Field("lastname") String value;
}
JustAnArbitraryClassWithMatchingFieldNames probe = new JustAnArbitraryClassWithMatchingFieldNames();
probe.value = "stark";
Example example = Example.of(probe, UntypedExampleMatcher.matching());
Query query = new Query(new Criteria().alike(example));
List<Person> result = template.find(query, Person.class);
----
====
Loading…
Cancel
Save