DATAMONGO-957 - Add support for query modifiers.

Using Meta allows to the define $comment, $maxScan, $maxTimeMS and $snapshot on query. When executed we add the meta information to the cursor in use.

We’ve introduced the @Meta annotation that allows to the define $comment, $maxScan, $maxTimeMS and $snapshot on a repository finder method.
Added tests to verify proper invocation of template methods
Use DBCursor.copy() for CursorPreparer.

Original pull request: #216.
This commit is contained in:
Christoph Strobl
2014-08-11 12:03:50 +02:00
committed by Thomas Darimont
parent 3597194742
commit a9306b99ec
10 changed files with 658 additions and 7 deletions

View File

@@ -2030,6 +2030,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
}
public DBCursor doInCollection(DBCollection collection) throws MongoException, DataAccessException {
if (fields == null || fields.toMap().isEmpty()) {
return collection.find(query);
} else {
@@ -2185,11 +2186,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
}
if (query.getSkip() <= 0 && query.getLimit() <= 0 && query.getSortObject() == null
&& !StringUtils.hasText(query.getHint())) {
&& !StringUtils.hasText(query.getHint()) && !query.getMeta().hasValues()) {
return cursor;
}
DBCursor cursorToUse = cursor;
DBCursor cursorToUse = cursor.copy();
try {
if (query.getSkip() > 0) {
@@ -2205,6 +2206,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
if (StringUtils.hasText(query.getHint())) {
cursorToUse = cursorToUse.hint(query.getHint());
}
if (query.getMeta().hasValues()) {
for (Entry<String, Object> entry : query.getMeta().values()) {
cursorToUse = cursorToUse.addSpecial(entry.getKey(), entry.getValue());
}
}
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e);
}

View File

@@ -0,0 +1,193 @@
/*
* Copyright 2014 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 java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Meta-data for {@link Query} instances.
*
* @author Christoph Strobl
* @author Oliver Gierke
* @since 1.6
*/
public class Meta {
private enum MetaKey {
MAX_TIME_MS("$maxTimeMS"), MAX_SCAN("$maxScan"), COMMENT("$comment"), SNAPSHOT("$snapshot");
private String key;
private MetaKey(String key) {
this.key = key;
}
}
private final Map<String, Object> values = new LinkedHashMap<String, Object>(2);
/**
* @return {@literal null} if not set.
*/
public Long getMaxTimeMsec() {
return getValue(MetaKey.MAX_TIME_MS.key);
}
/**
* Set the maximum time limit in milliseconds for processing operations.
*
* @param maxTimeMsec
*/
public void setMaxTimeMsec(long maxTimeMsec) {
setMaxTime(maxTimeMsec, TimeUnit.MILLISECONDS);
}
/**
* Set the maximum time limit for processing operations.
*
* @param timeout
* @param timeUnit
*/
public void setMaxTime(long timeout, TimeUnit timeUnit) {
setValue(MetaKey.MAX_TIME_MS.key, (timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS).toMillis(timeout));
}
/**
* @return {@literal null} if not set.
*/
public Long getMaxScan() {
return getValue(MetaKey.MAX_SCAN.key);
}
/**
* Only scan the specified number of documents.
*
* @param maxScan
*/
public void setMaxScan(long maxScan) {
setValue(MetaKey.MAX_SCAN.key, maxScan);
}
/**
* Add a comment to the query.
*
* @param comment
*/
public void setComment(String comment) {
setValue(MetaKey.COMMENT.key, comment);
}
/**
* @return {@literal null} if not set.
*/
public String getComment() {
return getValue(MetaKey.COMMENT.key);
}
/**
* Using snapshot prevents the cursor from returning a document more than once.
*
* @param useSnapshot
*/
public void setSnapshot(boolean useSnapshot) {
setValue(MetaKey.SNAPSHOT.key, useSnapshot);
}
/**
* @return {@literal null} if not set.
*/
public boolean getSnapshot() {
return getValue(MetaKey.SNAPSHOT.key, false);
}
/**
* @return
*/
public boolean hasValues() {
return !this.values.isEmpty();
}
/**
* Get {@link Iterable} of set meta values.
*
* @return
*/
public Iterable<Entry<String, Object>> values() {
return Collections.unmodifiableSet(this.values.entrySet());
}
/**
* Sets or removes the value in case of {@literal null} or empty {@link String}.
*
* @param key must not be {@literal null} or empty.
* @param value
*/
private void setValue(String key, Object value) {
Assert.hasText(key, "Meta key must not be 'null' or blank.");
if (value == null || (value instanceof String && !StringUtils.hasText((String) value))) {
this.values.remove(key);
}
this.values.put(key, value);
}
@SuppressWarnings("unchecked")
private <T> T getValue(String key) {
return (T) this.values.get(key);
}
private <T> T getValue(String key, T defaultValue) {
T value = getValue(key);
return value != null ? value : defaultValue;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(this.values);
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Meta)) {
return false;
}
Meta other = (Meta) obj;
return ObjectUtils.nullSafeEquals(this.values, other.values);
}
}

View File

@@ -25,6 +25,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@@ -53,6 +54,8 @@ public class Query {
private int limit;
private String hint;
private Meta meta = new Meta();
/**
* Static factory method to create a {@link Query} using the provided {@link CriteriaDefinition}.
*
@@ -275,6 +278,84 @@ public class Query {
return hint;
}
/**
* @param maxTimeMsec
* @return
* @see Meta#setMaxTimeMsec(long)
* @since 1.6
*/
public Query maxTimeMsec(long maxTimeMsec) {
meta.setMaxTimeMsec(maxTimeMsec);
return this;
}
/**
* @param timeout
* @param timeUnit
* @return
* @see Meta#setMaxTime(long, TimeUnit)
* @since 1.6
*/
public Query maxTime(long timeout, TimeUnit timeUnit) {
meta.setMaxTime(timeout, timeUnit);
return this;
}
/**
* @param maxScan
* @return
* @see Meta#setMaxScan(long)
* @since 1.6
*/
public Query maxScan(long maxScan) {
meta.setMaxScan(maxScan);
return this;
}
/**
* @param comment
* @return
* @see Meta#setComment(String)
* @since 1.6
*/
public Query comment(String comment) {
meta.setComment(comment);
return this;
}
/**
* @return
* @see Meta#setSnapshot(boolean)
* @since 1.6
*/
public Query useSnapshot() {
meta.setSnapshot(true);
return this;
}
/**
* @return never {@literal null}.
* @since 1.6
*/
public Meta getMeta() {
return meta;
}
/**
* @param meta must not be {@literal null}.
* @since 1.6
*/
public void setMeta(Meta meta) {
Assert.notNull(meta, "Query meta might be empty but must not be null.");
this.meta = meta;
}
protected List<CriteriaDefinition> getCriteria() {
return new ArrayList<CriteriaDefinition>(this.criteria.values());
}
@@ -312,8 +393,9 @@ public class Query {
boolean hintEqual = this.hint == null ? that.hint == null : this.hint.equals(that.hint);
boolean skipEqual = this.skip == that.skip;
boolean limitEqual = this.limit == that.limit;
boolean metaEqual = nullSafeEquals(this.meta, that.meta);
return criteriaEqual && fieldsEqual && sortEqual && hintEqual && skipEqual && limitEqual;
return criteriaEqual && fieldsEqual && sortEqual && hintEqual && skipEqual && limitEqual && metaEqual;
}
/*
@@ -331,6 +413,7 @@ public class Query {
result += 31 * nullSafeHashCode(hint);
result += 31 * skip;
result += 31 * limit;
result += 31 * nullSafeHashCode(meta);
return result;
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2014 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.repository;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.QueryAnnotation;
/**
* @author Christoph Strobl
* @since 1.6
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@QueryAnnotation
public @interface Meta {
/**
* Set the maximum time limit in milliseconds for processing operations.
*
* @return
*/
long maxExcecutionTime() default -1;
/**
* Only scan the specified number of documents.
*
* @return
*/
long maxScanDocuments() default -1;
/**
* Add a comment to the query.
*
* @return
*/
String comment() default "";
/**
* Using snapshot prevents the cursor from returning a document more than once.
*
* @return
*/
boolean snapshot() default false;
}

View File

@@ -86,6 +86,8 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
MongoParameterAccessor accessor = new MongoParametersParameterAccessor(method, parameters);
Query query = createQuery(new ConvertingParameterAccessor(operations.getConverter(), accessor));
applyQueryMetaAttributesWhenPresent(query);
Object result = null;
if (isDeleteQuery()) {
@@ -121,6 +123,14 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
return CONVERSION_SERVICE.convert(result, expectedReturnType);
}
private Query applyQueryMetaAttributesWhenPresent(Query query) {
if (method.hasQueryMetaAttributes()) {
query.setMeta(method.getQueryMetaAttributes());
}
return query;
}
/**
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
* {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be
@@ -130,7 +140,12 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
* @return
*/
protected Query createCountQuery(ConvertingParameterAccessor accessor) {
return createQuery(accessor);
Query query = createQuery(accessor);
applyQueryMetaAttributesWhenPresent(query);
return query;
}
/**

View File

@@ -27,6 +27,7 @@ import org.springframework.data.geo.GeoResults;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.repository.Meta;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryMethod;
@@ -182,4 +183,55 @@ public class MongoQueryMethod extends QueryMethod {
TypeInformation<?> getReturnType() {
return ClassTypeInformation.fromReturnTypeOf(method);
}
/**
* @return return true if {@link Meta} annotation is available.
* @since 1.6
*/
public boolean hasQueryMetaAttributes() {
return getMetaAnnotation() != null;
}
/**
* Returns the {@link Meta} annotation that is applied to the method or {@code null} if not available.
*
* @return
* @since 1.6
*/
Meta getMetaAnnotation() {
return method.getAnnotation(Meta.class);
}
/**
* Returns the {@link org.springframework.data.mongodb.core.query.Meta} attributes to be applied.
*
* @return never {@literal null}.
* @since 1.6
*/
public org.springframework.data.mongodb.core.query.Meta getQueryMetaAttributes() {
Meta meta = getMetaAnnotation();
if (meta == null) {
return new org.springframework.data.mongodb.core.query.Meta();
}
org.springframework.data.mongodb.core.query.Meta metaAttributes = new org.springframework.data.mongodb.core.query.Meta();
if (meta.maxExcecutionTime() > 0) {
metaAttributes.setMaxTimeMsec(meta.maxExcecutionTime());
}
if (meta.maxScanDocuments() > 0) {
metaAttributes.setMaxScan(meta.maxScanDocuments());
}
if (StringUtils.hasText(meta.comment())) {
metaAttributes.setComment(meta.comment());
}
if (meta.snapshot()) {
metaAttributes.setSnapshot(meta.snapshot());
}
return metaAttributes;
}
}

View File

@@ -89,6 +89,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
@Before
public void setUp() {
when(cursor.copy()).thenReturn(cursor);
when(factory.getDb()).thenReturn(db);
when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator);
when(db.getCollection(Mockito.any(String.class))).thenReturn(collection);

View File

@@ -15,16 +15,21 @@
*/
package org.springframework.data.mongodb.core;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate.QueryCursorPreparer;
import org.springframework.data.mongodb.core.query.Meta;
import org.springframework.data.mongodb.core.query.Query;
import com.mongodb.DBCursor;
@@ -41,6 +46,13 @@ public class QueryCursorPreparerUnitTests {
@Mock MongoDbFactory factory;
@Mock DBCursor cursor;
@Mock DBCursor cursorToUse;
@Before
public void setUp() {
when(cursor.copy()).thenReturn(cursorToUse);
}
/**
* @see DATAMONGO-185
*/
@@ -49,9 +61,81 @@ public class QueryCursorPreparerUnitTests {
Query query = query(where("foo").is("bar")).withHint("hint");
CursorPreparer preparer = new MongoTemplate(factory).new QueryCursorPreparer(query, null);
preparer.prepare(cursor);
pepare(query);
verify(cursor).hint("hint");
verify(cursorToUse).hint("hint");
}
/**
* @see DATAMONGO-957
*/
@Test
public void doesNotApplyMetaWhenEmpty() {
Query query = query(where("foo").is("bar"));
query.setMeta(new Meta());
pepare(query);
verify(cursor, never()).copy();
verify(cursorToUse, never()).addSpecial(any(String.class), anyObject());
}
/**
* @see DATAMONGO-957
*/
@Test
public void appliesMaxScanCorrectly() {
Query query = query(where("foo").is("bar")).maxScan(100);
pepare(query);
verify(cursorToUse).addSpecial(eq("$maxScan"), eq(100L));
}
/**
* @see DATAMONGO-957
*/
@Test
public void appliesMaxTimeCorrectly() {
Query query = query(where("foo").is("bar")).maxTime(1, TimeUnit.SECONDS);
pepare(query);
verify(cursorToUse).addSpecial(eq("$maxTimeMS"), eq(1000L));
}
/**
* @see DATAMONGO-957
*/
@Test
public void appliesCommentCorrectly() {
Query query = query(where("foo").is("bar")).comment("spring data");
pepare(query);
verify(cursorToUse).addSpecial(eq("$comment"), eq("spring data"));
}
/**
* @see DATAMONGO-957
*/
@Test
public void appliesSnapshotCorrectly() {
Query query = query(where("foo").is("bar")).useSnapshot();
pepare(query);
verify(cursorToUse).addSpecial(eq("$snapshot"), eq(true));
}
private DBCursor pepare(Query query) {
CursorPreparer preparer = new MongoTemplate(factory).new QueryCursorPreparer(query, null);
return preparer.prepare(cursor);
}
}

View File

@@ -29,9 +29,13 @@ import org.hamcrest.core.Is;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.Person;
@@ -42,6 +46,7 @@ import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.Meta;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -140,6 +145,70 @@ public class AbstractMongoQueryUnitTests {
Matchers.eq("persons"));
}
/**
* @see DATAMONGO-957
*/
@Test
public void metadataShouldNotBeAddedToQueryWhenNotPresent() {
MongoQueryFake query = createQueryForMethod("findByFirstname", String.class);
query.execute(new Object[] { "fake" });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(this.mongoOperationsMock, times(1))
.find(captor.capture(), Matchers.eq(Person.class), Matchers.eq("persons"));
assertThat(captor.getValue().getMeta().getComment(), nullValue());
}
/**
* @see DATAMONGO-957
*/
@Test
public void metadataShouldBeAddedToQueryCorrectly() {
MongoQueryFake query = createQueryForMethod("findByFirstname", String.class, Pageable.class);
query.execute(new Object[] { "fake", new PageRequest(0, 10) });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(this.mongoOperationsMock, times(1))
.find(captor.capture(), Matchers.eq(Person.class), Matchers.eq("persons"));
assertThat(captor.getValue().getMeta().getComment(), is("comment"));
}
/**
* @see DATAMONGO-957
*/
@Test
public void metadataShouldBeAddedToCountQueryCorrectly() {
MongoQueryFake query = createQueryForMethod("findByFirstname", String.class, Pageable.class);
query.execute(new Object[] { "fake", new PageRequest(0, 10) });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(this.mongoOperationsMock, times(1)).count(captor.capture(), Matchers.eq("persons"));
assertThat(captor.getValue().getMeta().getComment(), is("comment"));
}
/**
* @see DATAMONGO-957
*/
@Test
public void metadataShouldBeAddedToStringBasedQueryCorrectly() {
MongoQueryFake query = createQueryForMethod("findByAnnotatedQuery", String.class, Pageable.class);
query.execute(new Object[] { "fake", new PageRequest(0, 10) });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(this.mongoOperationsMock, times(1))
.find(captor.capture(), Matchers.eq(Person.class), Matchers.eq("persons"));
assertThat(captor.getValue().getMeta().getComment(), is("comment"));
}
private MongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
try {
@@ -191,5 +260,15 @@ public class AbstractMongoQueryUnitTests {
List<Person> deleteByLastname(String lastname);
Long deletePersonByLastname(String lastname);
List<Person> findByFirstname(String firstname);
@Meta(comment = "comment")
Page<Person> findByFirstname(String firstnanme, Pageable pageable);
@Meta(comment = "comment")
@org.springframework.data.mongodb.repository.Query("{}")
Page<Person> findByAnnotatedQuery(String firstnanme, Pageable pageable);
}
}

View File

@@ -34,6 +34,7 @@ import org.springframework.data.mongodb.core.User;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.Address;
import org.springframework.data.mongodb.repository.Contact;
import org.springframework.data.mongodb.repository.Meta;
import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
@@ -121,6 +122,61 @@ public class MongoQueryMethodUnitTests {
new MongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository2.class), context);
}
/**
* @see DATAMONGO-957
*/
@Test
public void createsMongoQueryMethodWithEmptyMetaCorrectly() throws Exception {
MongoQueryMethod method = queryMethod("emptyMetaAnnotation");
assertThat(method.hasQueryMetaAttributes(), is(true));
assertThat(method.getQueryMetaAttributes().hasValues(), is(false));
}
/**
* @see DATAMONGO-957
*/
@Test
public void createsMongoQueryMethodWithMaxExecutionTimeCorrectly() throws Exception {
MongoQueryMethod method = queryMethod("metaWithMaxExecutionTime");
assertThat(method.hasQueryMetaAttributes(), is(true));
assertThat(method.getQueryMetaAttributes().getMaxTimeMsec(), is(100L));
}
/**
* @see DATAMONGO-957
*/
@Test
public void createsMongoQueryMethodWithMaxScanCorrectly() throws Exception {
MongoQueryMethod method = queryMethod("metaWithMaxScan");
assertThat(method.hasQueryMetaAttributes(), is(true));
assertThat(method.getQueryMetaAttributes().getMaxScan(), is(10L));
}
/**
* @see DATAMONGO-957
*/
@Test
public void createsMongoQueryMethodWithCommentCorrectly() throws Exception {
MongoQueryMethod method = queryMethod("metaWithComment");
assertThat(method.hasQueryMetaAttributes(), is(true));
assertThat(method.getQueryMetaAttributes().getComment(), is("foo bar"));
}
/**
* @see DATAMONGO-957
*/
@Test
public void createsMongoQueryMethodWithSnapshotCorrectly() throws Exception {
MongoQueryMethod method = queryMethod("metaWithSnapshotUsage");
assertThat(method.hasQueryMetaAttributes(), is(true));
assertThat(method.getQueryMetaAttributes().getSnapshot(), is(true));
}
private MongoQueryMethod queryMethod(String name, Class<?>... parameters) throws Exception {
Method method = PersonRepository.class.getMethod(name, parameters);
return new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class), context);
@@ -138,6 +194,22 @@ public class MongoQueryMethodUnitTests {
GeoResults<User> findByFirstname(String firstname, Point location);
Collection<GeoResult<User>> findByLastname(String lastname, Point location);
@Meta
List<User> emptyMetaAnnotation();
@Meta(maxExcecutionTime = 100)
List<User> metaWithMaxExecutionTime();
@Meta(maxScanDocuments = 10)
List<User> metaWithMaxScan();
@Meta(comment = "foo bar")
List<User> metaWithComment();
@Meta(snapshot = true)
List<User> metaWithSnapshotUsage();
}
interface SampleRepository extends Repository<Contact, Long> {
@@ -155,4 +227,5 @@ public class MongoQueryMethodUnitTests {
interface Customer {
}
}