Skip to content

DATAMONGO-1768 - Allow ignoring type restriction when issuing QBE. #496

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
DATAMONGO-1768 - Introduce UntypedExample.
Introducing UntypedExample allows creation of QBE criteria that does not infer a strict type match.
  • Loading branch information
christophstrobl committed Aug 21, 2017
commit 6e9abbf2ab200bb61b7df76c2ebbe493ca815258
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
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.UntypedExample;
import org.springframework.data.support.ExampleMatcherAccessor;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -293,7 +294,7 @@ private Document updateTypeRestrictions(Document query, Example example) {

Document result = new Document();

if (isTypeRestricting(example.getMatcher())) {
if (isTypeRestricting(example)) {

result.putAll(query);
this.converter.getTypeMapper().writeTypeRestrictions(result, getTypesToMatch(example));
Expand All @@ -309,13 +310,17 @@ private Document updateTypeRestrictions(Document query, Example example) {
return result;
}

private boolean isTypeRestricting(ExampleMatcher matcher) {
private boolean isTypeRestricting(Example example) {

if (matcher.getIgnoredPaths().isEmpty()) {
if(example instanceof UntypedExample) {
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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;

/**
* {@link Example} implementation for query by example (QBE). Unlike plain {@link Example} 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
*/
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
public class UntypedExample implements Example<Object> {

private final @NonNull Object probe;
private final @NonNull ExampleMatcher matcher;

/**
* Create a new {@literal untyped} {@link Example} including all non-null properties by default.
*
* @param probe must not be {@literal null}.
* @return new instance of {@link Example}.
*/
public static Example<Object> of(Object probe) {
return new UntypedExample(probe, ExampleMatcher.matching());
}

/**
* Create a new {@literal untyped} {@link Example} using the given {@link ExampleMatcher}.
*
* @param probe must not be {@literal null}.
* @param matcher must not be {@literal null}.
* @return new instance of {@link Example}.
*/
public static Example<Object> of(Object probe, ExampleMatcher matcher) {
return new UntypedExample(probe, matcher);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
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.UntypedExample;

import com.mongodb.MongoClient;

Expand Down Expand Up @@ -179,7 +180,7 @@ public void typedExampleMatchesNothingIfTypesDoNotMatch() {
}

@Test // DATAMONGO-1768
public void untypedExampleMatchesCorrectly() {
public void exampleIgnoringClassTypeKeyMatchesCorrectly() {

NotAPersonButStillMatchingFields probe = new NotAPersonButStillMatchingFields();
probe.lastname = "stark";
Expand All @@ -192,6 +193,19 @@ public void untypedExampleMatchesCorrectly() {
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(UntypedExample.of(probe)));
List<Person> result = operations.find(query, Person.class);

assertThat(result, hasSize(2));
assertThat(result, hasItems(p1, p3));
}

@Document(collection = "dramatis-personae")
@EqualsAndHashCode
@ToString
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
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.UntypedExample;
import org.springframework.data.mongodb.test.util.IsBsonObject;
import org.springframework.data.util.TypeInformation;

Expand Down Expand Up @@ -470,7 +471,7 @@ public void writeTypeRestrictions(org.bson.Document result, Set<Class<?>> restri

@Override
public void writeType(TypeInformation<?> info, Bson sink) {
((org.bson.Document) sink).put("_foo", "bar");
((org.bson.Document) sink).put("_foo", "bar");

}
});
Expand All @@ -482,6 +483,17 @@ public void writeType(TypeInformation<?> info, Bson sink) {
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(UntypedExample.of(probe));
assertThat(document, isBsonObject().notContaining("_class"));
}

static class FlatDocument {

@Id String id;
Expand Down
26 changes: 26 additions & 0 deletions src/main/asciidoc/reference/query-by-example.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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] }`.

`Untyped` example bypasses the default behavior and skips 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 = UntypedExample.of(probe);

Query query = new Query(new Criteria().alike(example));
List<Person> result = template.find(query, Person.class);
----
====