DATAMONGO-1854 - Add collation option to @Document and @Query annotation.
We now allow to specify the collation via the @Query annotation.
public interface PersonRepository extends MongoRepository<Person, String> {
@Query(collation = "en_US")
List<Person> findByFirstname(String firstname);
@Query(collation = "{ 'locale' : 'en_US' }")
List<Person> findPersonByFirstname(String firstname);
@Query(collation = "?1")
List<Person> findByFirstname(String firstname, Object collation);
@Query(collation = "{ 'locale' : '?1' }")
List<Person> findByFirstname(String firstname, String collation);
List<Person> findByFirstname(String firstname, Collation collation);
}
We now make sure to include collation information derived from the Query method if the collation is a fixed value.
Original pull request: #644.
This commit is contained in:
committed by
Mark Paluch
parent
5c333d6159
commit
b8368a677d
@@ -118,6 +118,23 @@ public class Collation {
|
||||
return new Collation(locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given collation string into a {@link Collation}.
|
||||
*
|
||||
* @param collation the collation to parse. Can be a simple string like {@code en_US} or a
|
||||
* {@link Document#parse(String) parsable} document like <code>{ 'locale' : '?0' }</code> .
|
||||
* @return never {@literal null}.
|
||||
* @throws IllegalArgumentException if {@literal collation} is null.
|
||||
* @since 2.2
|
||||
*/
|
||||
public static Collation parse(String collation) {
|
||||
|
||||
Assert.notNull(collation, "Collation must not be null!");
|
||||
|
||||
return StringUtils.trimLeadingWhitespace(collation).startsWith("{") ? from(Document.parse(collation))
|
||||
: of(collation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new {@link Collation} from values in {@link Document}.
|
||||
*
|
||||
|
||||
@@ -95,4 +95,36 @@ public @interface Query {
|
||||
* @since 2.1
|
||||
*/
|
||||
String sort() default "";
|
||||
|
||||
/**
|
||||
* Defines the collation to apply when executing the query. <br />
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
*
|
||||
* // Fixed value
|
||||
* @Query(collation = "en_US")
|
||||
* List<Entry> findAllByFixedCollation();
|
||||
*
|
||||
* // Fixed value as Document
|
||||
* @Query(collation = "{ 'locale' : 'en_US' }")
|
||||
* List<Entry> findAllByFixedJsonCollation();
|
||||
*
|
||||
* // Dynamic value as String
|
||||
* @Query(collation = "?0")
|
||||
* List<Entry> findAllByDynamicCollation(String collation);
|
||||
*
|
||||
* // Dynamic value as Document
|
||||
* @Query(collation = "{ 'locale' : ?0 }")
|
||||
* List<Entry> findAllByDynamicJsonCollation(String collation);
|
||||
*
|
||||
* // SpEL expression
|
||||
* @Query(collation = "?#{[0]}")
|
||||
* List<Entry> findAllByDynamicSpElCollation(String collation);
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* @since 2.2
|
||||
*/
|
||||
String collation() default "";
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
|
||||
|
||||
applyQueryMetaAttributesWhenPresent(query);
|
||||
query = applyAnnotatedDefaultSortIfPresent(query);
|
||||
query = applyAnnotatedCollationIfPresent(query, accessor);
|
||||
|
||||
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor);
|
||||
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
|
||||
@@ -154,6 +155,21 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
|
||||
return QueryUtils.decorateSort(query, Document.parse(method.getAnnotatedSort()));
|
||||
}
|
||||
|
||||
/**
|
||||
* If present apply a {@link org.springframework.data.mongodb.core.query.Collation} derived from the
|
||||
* {@link org.springframework.data.repository.query.QueryMethod} the given {@link Query}.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @param accessor the {@link ParameterAccessor} used to obtain parameter placeholder replacement values.
|
||||
* @return
|
||||
* @since 2.2
|
||||
*/
|
||||
Query applyAnnotatedCollationIfPresent(Query query, ConvertingParameterAccessor accessor) {
|
||||
|
||||
return QueryUtils.applyCollation(query, method.hasAnnotatedCollation() ? method.getAnnotatedCollation() : null,
|
||||
accessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -105,12 +105,14 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
|
||||
|
||||
private Object execute(MongoParameterAccessor parameterAccessor) {
|
||||
|
||||
Query query = createQuery(new ConvertingParameterAccessor(operations.getConverter(), parameterAccessor));
|
||||
ConvertingParameterAccessor convertingParamterAccessor = new ConvertingParameterAccessor(operations.getConverter(), parameterAccessor);
|
||||
Query query = createQuery(convertingParamterAccessor);
|
||||
|
||||
applyQueryMetaAttributesWhenPresent(query);
|
||||
query = applyAnnotatedDefaultSortIfPresent(query);
|
||||
query = applyAnnotatedCollationIfPresent(query, convertingParamterAccessor);
|
||||
|
||||
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor);
|
||||
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(convertingParamterAccessor);
|
||||
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
|
||||
|
||||
FindWithQuery<?> find = typeToRead == null //
|
||||
@@ -119,7 +121,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
|
||||
|
||||
String collection = method.getEntityInformation().getCollectionName();
|
||||
|
||||
ReactiveMongoQueryExecution execution = getExecution(parameterAccessor,
|
||||
ReactiveMongoQueryExecution execution = getExecution(convertingParamterAccessor,
|
||||
new ResultProcessingConverter(processor, operations, instantiators), find);
|
||||
|
||||
return execution.execute(query, processor.getReturnedType().getDomainType(), collection);
|
||||
@@ -195,6 +197,21 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
|
||||
return QueryUtils.decorateSort(query, Document.parse(method.getAnnotatedSort()));
|
||||
}
|
||||
|
||||
/**
|
||||
* If present apply a {@link org.springframework.data.mongodb.core.query.Collation} derived from the
|
||||
* {@link org.springframework.data.repository.query.QueryMethod} the given {@link Query}.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @param accessor the {@link ParameterAccessor} used to obtain parameter placeholder replacement values.
|
||||
* @return
|
||||
* @since 2.2
|
||||
*/
|
||||
Query applyAnnotatedCollationIfPresent(Query query, ConvertingParameterAccessor accessor) {
|
||||
|
||||
return QueryUtils.applyCollation(query, method.hasAnnotatedCollation() ? method.getAnnotatedCollation() : null,
|
||||
accessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.mongodb.core.convert.MongoWriter;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.core.query.TextCriteria;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
@@ -135,6 +136,15 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor {
|
||||
return delegate.getFullText();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getCollation()
|
||||
*/
|
||||
@Override
|
||||
public Collation getCollation() {
|
||||
return delegate.getCollation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given value with the underlying {@link MongoWriter}.
|
||||
*
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.repository.query;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.core.query.TextCriteria;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -57,6 +58,15 @@ public interface MongoParameterAccessor extends ParameterAccessor {
|
||||
@Nullable
|
||||
TextCriteria getFullText();
|
||||
|
||||
/**
|
||||
* Returns the {@link Collation} to be used for the query.
|
||||
*
|
||||
* @return {@literal null} if not set.
|
||||
* @since 2.2
|
||||
*/
|
||||
@Nullable
|
||||
Collation getCollation();
|
||||
|
||||
/**
|
||||
* Returns the raw parameter values of the underlying query method.
|
||||
*
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.core.query.TextCriteria;
|
||||
import org.springframework.data.mongodb.repository.Near;
|
||||
import org.springframework.data.mongodb.repository.query.MongoParameters.MongoParameter;
|
||||
@@ -45,12 +46,13 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
|
||||
private final int maxDistanceIndex;
|
||||
private final @Nullable Integer fullTextIndex;
|
||||
private final @Nullable Integer nearIndex;
|
||||
private final @Nullable Integer collationIndex;
|
||||
|
||||
/**
|
||||
* Creates a new {@link MongoParameters} instance from the given {@link Method} and {@link MongoQueryMethod}.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @param queryMethod must not be {@literal null}.
|
||||
* @param isGeoNearMethod indicate if this is a geo spatial query method
|
||||
*/
|
||||
public MongoParameters(Method method, boolean isGeoNearMethod) {
|
||||
|
||||
@@ -64,6 +66,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
|
||||
|
||||
this.rangeIndex = getTypeIndex(parameterTypeInfo, Range.class, Distance.class);
|
||||
this.maxDistanceIndex = this.rangeIndex == -1 ? getTypeIndex(parameterTypeInfo, Distance.class, null) : -1;
|
||||
this.collationIndex = getTypeIndex(parameterTypeInfo, Collation.class, null);
|
||||
|
||||
int index = findNearIndexInParameters(method);
|
||||
if (index == -1 && isGeoNearMethod) {
|
||||
@@ -74,7 +77,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
|
||||
}
|
||||
|
||||
private MongoParameters(List<MongoParameter> parameters, int maxDistanceIndex, @Nullable Integer nearIndex,
|
||||
@Nullable Integer fullTextIndex, int rangeIndex) {
|
||||
@Nullable Integer fullTextIndex, int rangeIndex, @Nullable Integer collationIndex) {
|
||||
|
||||
super(parameters);
|
||||
|
||||
@@ -82,6 +85,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
|
||||
this.fullTextIndex = fullTextIndex;
|
||||
this.maxDistanceIndex = maxDistanceIndex;
|
||||
this.rangeIndex = rangeIndex;
|
||||
this.collationIndex = collationIndex;
|
||||
}
|
||||
|
||||
private final int getNearIndex(List<Class<?>> parameterTypes) {
|
||||
@@ -111,11 +115,11 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
|
||||
|
||||
MongoParameter param = createParameter(MethodParameter.forParameter(p));
|
||||
if (param.isManuallyAnnotatedNearParameter()) {
|
||||
if(index == -1) {
|
||||
if (index == -1) {
|
||||
index = param.getIndex();
|
||||
} else {
|
||||
throw new IllegalStateException(String.format("Found multiple @Near annotations ond method %s! Only one allowed!",
|
||||
method.toString()));
|
||||
throw new IllegalStateException(
|
||||
String.format("Found multiple @Near annotations ond method %s! Only one allowed!", method.toString()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -132,8 +136,6 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
|
||||
return new MongoParameter(parameter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int getDistanceRangeIndex() {
|
||||
return -1;
|
||||
}
|
||||
@@ -158,7 +160,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns ths inde of the parameter to be used as a textquery param
|
||||
* Returns the index of the parameter to be used as a textquery param
|
||||
*
|
||||
* @return
|
||||
* @since 1.6
|
||||
@@ -183,13 +185,24 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
|
||||
return rangeIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the {@link Collation} parameter or -1 if not present.
|
||||
*
|
||||
* @return -1 if not set.
|
||||
* @since 2.2
|
||||
*/
|
||||
public int getCollationParameterIndex() {
|
||||
return collationIndex != null ? collationIndex.intValue() : -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List)
|
||||
*/
|
||||
@Override
|
||||
protected MongoParameters createFrom(List<MongoParameter> parameters) {
|
||||
return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex);
|
||||
return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex,
|
||||
this.collationIndex);
|
||||
}
|
||||
|
||||
private int getTypeIndex(List<TypeInformation<?>> parameterTypes, Class<?> type, @Nullable Class<?> componentType) {
|
||||
@@ -241,7 +254,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
|
||||
@Override
|
||||
public boolean isSpecialParameter() {
|
||||
return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) || isNearParameter()
|
||||
|| TextCriteria.class.isAssignableFrom(getType());
|
||||
|| TextCriteria.class.isAssignableFrom(getType()) || Collation.class.isAssignableFrom(getType());
|
||||
}
|
||||
|
||||
private boolean isNearParameter() {
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.domain.Range.Bound;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.core.query.Term;
|
||||
import org.springframework.data.mongodb.core.query.TextCriteria;
|
||||
import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
@@ -67,7 +68,8 @@ public class MongoParametersParameterAccessor extends ParametersParameterAccesso
|
||||
}
|
||||
|
||||
int maxDistanceIndex = mongoParameters.getMaxDistanceIndex();
|
||||
Bound<Distance> maxDistance = maxDistanceIndex == -1 ? Bound.unbounded() : Bound.inclusive((Distance) getValue(maxDistanceIndex));
|
||||
Bound<Distance> maxDistance = maxDistanceIndex == -1 ? Bound.unbounded()
|
||||
: Bound.inclusive((Distance) getValue(maxDistanceIndex));
|
||||
|
||||
return Range.of(Bound.unbounded(), maxDistance);
|
||||
}
|
||||
@@ -134,6 +136,20 @@ public class MongoParametersParameterAccessor extends ParametersParameterAccesso
|
||||
ClassUtils.getShortName(fullText.getClass())));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getCollation()
|
||||
*/
|
||||
@Override
|
||||
public Collation getCollation() {
|
||||
|
||||
if (method.getParameters().getCollationParameterIndex() == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getValue(method.getParameters().getCollationParameterIndex());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues()
|
||||
|
||||
@@ -322,6 +322,30 @@ public class MongoQueryMethod extends QueryMethod {
|
||||
"Expected to find @Query annotation but did not. Make sure to check hasAnnotatedSort() before."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the query method is decorated with an non empty {@link Query#collation()}.
|
||||
*
|
||||
* @return true if method annotated with {@link Query} having an non empty collation attribute.
|
||||
* @since 2.2
|
||||
*/
|
||||
public boolean hasAnnotatedCollation() {
|
||||
return lookupQueryAnnotation().map(it -> !it.collation().isEmpty()).orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collation value extracted from the {@link Query} annotation.
|
||||
*
|
||||
* @return the {@link Query#sort()} value.
|
||||
* @throws IllegalStateException if method not annotated with {@link Query}. Make sure to check
|
||||
* {@link #hasAnnotatedQuery()} first.
|
||||
* @since 2.2
|
||||
*/
|
||||
public String getAnnotatedCollation() {
|
||||
|
||||
return lookupQueryAnnotation().map(Query::collation).orElseThrow(() -> new IllegalStateException(
|
||||
"Expected to find @Query annotation but did not. Make sure to check hasAnnotatedCollation() before."));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
|
||||
|
||||
|
||||
@@ -15,10 +15,18 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.repository.query;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.bson.Document;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.NumberUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Internal utility class to help avoid duplicate code required in both the reactive and the sync {@link Query} support
|
||||
@@ -30,6 +38,8 @@ import org.springframework.data.mongodb.core.query.Query;
|
||||
*/
|
||||
class QueryUtils {
|
||||
|
||||
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
|
||||
|
||||
/**
|
||||
* Decorate {@link Query} and add a default sort expression to the given {@link Query}. Attributes of the given
|
||||
* {@code sort} may be overwritten by the sort explicitly defined by the {@link Query} itself.
|
||||
@@ -58,4 +68,58 @@ class QueryUtils {
|
||||
|
||||
return (Query) factory.getProxy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a collation extracted from the given {@literal collationExpression} to the given {@link Query}. Potentially
|
||||
* replace parameter placeholders with values from the {@link ConvertingParameterAccessor accessor}.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @param collationExpression must not be {@literal null}.
|
||||
* @param accessor must not be {@literal null}.
|
||||
* @return the {@link Query} having proper {@link Collation}.
|
||||
* @see Query#collation(Collation)
|
||||
* @since 2.2
|
||||
*/
|
||||
static Query applyCollation(Query query, @Nullable String collationExpression, ConvertingParameterAccessor accessor) {
|
||||
|
||||
if (accessor.getCollation() != null) {
|
||||
return query.collation(accessor.getCollation());
|
||||
}
|
||||
|
||||
if (collationExpression == null) {
|
||||
return query;
|
||||
}
|
||||
|
||||
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(collationExpression);
|
||||
|
||||
// TODO: use parameter binding Parser instead of Document.parse once DATAMONGO-2199 is merged.
|
||||
|
||||
if (!matcher.find()) {
|
||||
return query.collation(Collation.parse(collationExpression));
|
||||
}
|
||||
|
||||
String placeholder = matcher.group();
|
||||
Object placeholderValue = accessor.getBindableValue(computeParameterIndex(placeholder));
|
||||
|
||||
if (collationExpression.startsWith("?")) {
|
||||
|
||||
if (placeholderValue instanceof String) {
|
||||
return query.collation(Collation.parse(placeholderValue.toString()));
|
||||
}
|
||||
if (placeholderValue instanceof Locale) {
|
||||
return query.collation(Collation.of((Locale) placeholderValue));
|
||||
}
|
||||
if (placeholderValue instanceof Document) {
|
||||
return query.collation(Collation.from((Document) placeholderValue));
|
||||
}
|
||||
throw new IllegalArgumentException(String.format("Collation must be a String, Locale or Document but was %s",
|
||||
ObjectUtils.nullSafeClassName(placeholderValue)));
|
||||
}
|
||||
|
||||
return query.collation(Collation.parse(collationExpression.replace(placeholder, placeholderValue.toString())));
|
||||
}
|
||||
|
||||
private static int computeParameterIndex(String parameter) {
|
||||
return NumberUtils.parseNumber(parameter.replace("?", ""), Integer.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.springframework.data.domain.Sort.Order;
|
||||
import org.springframework.data.mongodb.core.MongoOperations;
|
||||
import org.springframework.data.mongodb.core.index.Index;
|
||||
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.repository.query.MongoEntityMetadata;
|
||||
import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery;
|
||||
import org.springframework.data.repository.core.support.QueryCreationListener;
|
||||
@@ -93,6 +94,14 @@ class IndexEnsuringQueryCreationListener implements QueryCreationListener<PartTr
|
||||
}
|
||||
}
|
||||
|
||||
if (query.getQueryMethod().hasAnnotatedCollation()) {
|
||||
|
||||
String collation = query.getQueryMethod().getAnnotatedCollation();
|
||||
if (!collation.contains("?")) {
|
||||
index = index.collation(Collation.parse(collation));
|
||||
}
|
||||
}
|
||||
|
||||
MongoEntityMetadata<?> metadata = query.getQueryMethod().getEntityInformation();
|
||||
indexOperationsProvider.indexOps(metadata.getCollectionName()).ensureIndex(index);
|
||||
LOG.debug(String.format("Created %s!", index));
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.repository.query;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
@@ -24,6 +23,7 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.bson.Document;
|
||||
@@ -52,6 +52,7 @@ import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
|
||||
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.Collation;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.data.mongodb.repository.Meta;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
@@ -121,7 +122,7 @@ public class AbstractMongoQueryUnitTests {
|
||||
MongoQueryFake query = createQueryForMethod("deletePersonByLastname", String.class);
|
||||
query.setDeleteQuery(true);
|
||||
|
||||
assertThat(query.execute(new Object[] { "fake" }), is(0L));
|
||||
assertThat(query.execute(new Object[] { "fake" })).isEqualTo(0L);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-566, DATAMONGO-978
|
||||
@@ -133,7 +134,7 @@ public class AbstractMongoQueryUnitTests {
|
||||
MongoQueryFake query = createQueryForMethod("deletePersonByLastname", String.class);
|
||||
query.setDeleteQuery(true);
|
||||
|
||||
assertThat(query.execute(new Object[] { "fake" }), is(100L));
|
||||
assertThat(query.execute(new Object[] { "fake" })).isEqualTo(100L);
|
||||
verify(mongoOperationsMock, times(1)).remove(any(), eq(Person.class), eq("persons"));
|
||||
}
|
||||
|
||||
@@ -148,7 +149,8 @@ public class AbstractMongoQueryUnitTests {
|
||||
verify(executableFind).as(Person.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().getMeta().getComment(), nullValue());
|
||||
assertThat(captor.getValue().getMeta().getComment()).isNull();
|
||||
;
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-957
|
||||
@@ -162,7 +164,7 @@ public class AbstractMongoQueryUnitTests {
|
||||
verify(executableFind).as(Person.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().getMeta().getComment(), is("comment"));
|
||||
assertThat(captor.getValue().getMeta().getComment()).isEqualTo("comment");
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-957
|
||||
@@ -176,7 +178,7 @@ public class AbstractMongoQueryUnitTests {
|
||||
verify(executableFind).as(Person.class);
|
||||
verify(withQueryMock, atLeast(1)).matching(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().getMeta().getComment(), is("comment"));
|
||||
assertThat(captor.getValue().getMeta().getComment()).isEqualTo("comment");
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-957, DATAMONGO-1783
|
||||
@@ -190,7 +192,7 @@ public class AbstractMongoQueryUnitTests {
|
||||
verify(executableFind).as(Person.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().getMeta().getComment(), is("comment"));
|
||||
assertThat(captor.getValue().getMeta().getComment()).isEqualTo("comment");
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1057
|
||||
@@ -208,8 +210,8 @@ public class AbstractMongoQueryUnitTests {
|
||||
verify(executableFind, times(2)).as(Person.class);
|
||||
verify(withQueryMock, times(2)).matching(captor.capture());
|
||||
|
||||
assertThat(captor.getAllValues().get(0).getSkip(), is(0L));
|
||||
assertThat(captor.getAllValues().get(1).getSkip(), is(10L));
|
||||
assertThat(captor.getAllValues().get(0).getSkip()).isZero();
|
||||
assertThat(captor.getAllValues().get(1).getSkip()).isEqualTo(10);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1057
|
||||
@@ -227,8 +229,8 @@ public class AbstractMongoQueryUnitTests {
|
||||
verify(executableFind, times(2)).as(Person.class);
|
||||
verify(withQueryMock, times(2)).matching(captor.capture());
|
||||
|
||||
assertThat(captor.getAllValues().get(0).getLimit(), is(11));
|
||||
assertThat(captor.getAllValues().get(1).getLimit(), is(11));
|
||||
assertThat(captor.getAllValues().get(0).getLimit()).isEqualTo(11);
|
||||
assertThat(captor.getAllValues().get(1).getLimit()).isEqualTo(11);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1057
|
||||
@@ -247,8 +249,8 @@ public class AbstractMongoQueryUnitTests {
|
||||
verify(withQueryMock, times(2)).matching(captor.capture());
|
||||
|
||||
Document expectedSortObject = new Document().append("bar", -1);
|
||||
assertThat(captor.getAllValues().get(0).getSortObject(), is(expectedSortObject));
|
||||
assertThat(captor.getAllValues().get(1).getSortObject(), is(expectedSortObject));
|
||||
assertThat(captor.getAllValues().get(0).getSortObject()).isEqualTo(expectedSortObject);
|
||||
assertThat(captor.getAllValues().get(1).getSortObject()).isEqualTo(expectedSortObject);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1080
|
||||
@@ -260,7 +262,7 @@ public class AbstractMongoQueryUnitTests {
|
||||
|
||||
AbstractMongoQuery query = createQueryForMethod("findByLastname", String.class);
|
||||
|
||||
assertThat(query.execute(new Object[] { "lastname" }), is(reference));
|
||||
assertThat(query.execute(new Object[] { "lastname" })).isEqualTo(reference);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1865
|
||||
@@ -272,7 +274,7 @@ public class AbstractMongoQueryUnitTests {
|
||||
|
||||
AbstractMongoQuery query = createQueryForMethod("findFirstByLastname", String.class).setLimitingQuery(true);
|
||||
|
||||
assertThat(query.execute(new Object[] { "lastname" }), is(reference));
|
||||
assertThat(query.execute(new Object[] { "lastname" })).isEqualTo(reference);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1872
|
||||
@@ -294,7 +296,7 @@ public class AbstractMongoQueryUnitTests {
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getSortObject(), is(equalTo(new Document("age", 1))));
|
||||
assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("age", 1));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1979
|
||||
@@ -305,7 +307,127 @@ public class AbstractMongoQueryUnitTests {
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getSortObject(), is(equalTo(new Document("age", -1))));
|
||||
assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("age", -1));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyStaticAnnotatedCollation() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingSpimpleStringValueByFirstName", String.class) //
|
||||
.execute(new Object[] { "dalinar" });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyStaticAnnotatedCollationAsDocument() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingDocumentByFirstName", String.class) //
|
||||
.execute(new Object[] { "dalinar" });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyDynamicAnnotatedCollationAsString() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
|
||||
.execute(new Object[] { "dalinar", "en_US" });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyDynamicAnnotatedCollationAsDocument() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
|
||||
.execute(new Object[] { "dalinar", new Document("locale", "en_US") });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyDynamicAnnotatedCollationAsLocale() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
|
||||
.execute(new Object[] { "dalinar", Locale.US });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1854
|
||||
public void shouldThrowExceptionOnNonParsableCollation() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
|
||||
.execute(new Object[] { "dalinar", 100 });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyDynamicAnnotatedCollationIn() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingPlaceholderInDocumentByFirstName", String.class, String.class) //
|
||||
.execute(new Object[] { "dalinar", "en_US" });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyCollationParameter() {
|
||||
|
||||
Collation collation = Collation.of("en_US");
|
||||
createQueryForMethod("findWithCollationParameterByFirstName", String.class, Collation.class) //
|
||||
.execute(new Object[] { "dalinar", collation });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation()).contains(collation);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void collationParameterShouldOverrideAnnotation() {
|
||||
|
||||
Collation collation = Collation.of("de_AT");
|
||||
createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) //
|
||||
.execute(new Object[] { "dalinar", collation });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation()).contains(collation);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void collationParameterShouldNotBeAppliedWhenNullOverrideAnnotation() {
|
||||
|
||||
createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) //
|
||||
.execute(new Object[] { "dalinar", null });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
private MongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
|
||||
@@ -400,6 +522,23 @@ public class AbstractMongoQueryUnitTests {
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(sort = "{ age : 1 }")
|
||||
List<Person> findByAge(Integer age, Sort page);
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(collation = "en_US")
|
||||
List<Person> findWithCollationUsingSpimpleStringValueByFirstName(String firstname);
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
|
||||
List<Person> findWithCollationUsingDocumentByFirstName(String firstname);
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(collation = "?1")
|
||||
List<Person> findWithCollationUsingPlaceholderByFirstName(String firstname, Object collation);
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : '?1' }")
|
||||
List<Person> findWithCollationUsingPlaceholderInDocumentByFirstName(String firstname, String collation);
|
||||
|
||||
List<Person> findWithCollationParameterByFirstName(String firstname, Collation collation);
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
|
||||
List<Person> findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation);
|
||||
}
|
||||
|
||||
// DATAMONGO-1872
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright 2019 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.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.data.mongodb.core.Person;
|
||||
import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithQuery;
|
||||
import org.springframework.data.mongodb.core.ReactiveFindOperation.ReactiveFind;
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
|
||||
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
|
||||
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
|
||||
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.Collation;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @currentRead Way of Kings - Brandon Sanderson
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class AbstractReactiveMongoQueryUnitTests {
|
||||
|
||||
@Mock ReactiveMongoOperations mongoOperationsMock;
|
||||
@Mock BasicMongoPersistentEntity<?> persitentEntityMock;
|
||||
@Mock MongoMappingContext mappingContextMock;
|
||||
|
||||
@Mock ReactiveFind<?> executableFind;
|
||||
@Mock FindWithQuery<?> withQueryMock;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
doReturn("persons").when(persitentEntityMock).getCollection();
|
||||
doReturn(persitentEntityMock).when(mappingContextMock).getPersistentEntity(Mockito.any(Class.class));
|
||||
doReturn(persitentEntityMock).when(mappingContextMock).getRequiredPersistentEntity(Mockito.any(Class.class));
|
||||
doReturn(Person.class).when(persitentEntityMock).getType();
|
||||
|
||||
MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContextMock);
|
||||
converter.afterPropertiesSet();
|
||||
|
||||
doReturn(converter).when(mongoOperationsMock).getConverter();
|
||||
|
||||
doReturn(executableFind).when(mongoOperationsMock).query(any());
|
||||
doReturn(withQueryMock).when(executableFind).as(any());
|
||||
doReturn(withQueryMock).when(withQueryMock).matching(any());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyStaticAnnotatedCollation() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingSpimpleStringValueByFirstName", String.class) //
|
||||
.execute(new Object[] { "dalinar" });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyStaticAnnotatedCollationAsDocument() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingDocumentByFirstName", String.class) //
|
||||
.execute(new Object[] { "dalinar" });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyDynamicAnnotatedCollationAsString() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
|
||||
.execute(new Object[] { "dalinar", "en_US" });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyDynamicAnnotatedCollationAsDocument() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
|
||||
.execute(new Object[] { "dalinar", new Document("locale", "en_US") });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyDynamicAnnotatedCollationAsLocale() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
|
||||
.execute(new Object[] { "dalinar", Locale.US });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1854
|
||||
public void shouldThrowExceptionOnNonParsableCollation() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingPlaceholderByFirstName", String.class, Object.class) //
|
||||
.execute(new Object[] { "dalinar", 100 });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyDynamicAnnotatedCollationIn() {
|
||||
|
||||
createQueryForMethod("findWithCollationUsingPlaceholderInDocumentByFirstName", String.class, String.class) //
|
||||
.execute(new Object[] { "dalinar", "en_US" });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldApplyCollationParameter() {
|
||||
|
||||
Collation collation = Collation.of("en_US");
|
||||
createQueryForMethod("findWithCollationParameterByFirstName", String.class, Collation.class) //
|
||||
.execute(new Object[] { "dalinar", collation });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation()).contains(collation);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void collationParameterShouldOverrideAnnotation() {
|
||||
|
||||
Collation collation = Collation.of("de_AT");
|
||||
createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) //
|
||||
.execute(new Object[] { "dalinar", collation });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation()).contains(collation);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void collationParameterShouldNotBeAppliedWhenNullOverrideAnnotation() {
|
||||
|
||||
createQueryForMethod("findWithWithCollationParameterAndAnnotationByFirstName", String.class, Collation.class) //
|
||||
.execute(new Object[] { "dalinar", null });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
assertThat(captor.getValue().getCollation().map(Collation::toDocument))
|
||||
.contains(Collation.of("en_US").toDocument());
|
||||
}
|
||||
|
||||
private ReactiveMongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
|
||||
return createQueryForMethod(Repo.class, methodName, paramTypes);
|
||||
}
|
||||
|
||||
private ReactiveMongoQueryFake createQueryForMethod(Class<?> repository, String methodName, Class<?>... paramTypes) {
|
||||
|
||||
try {
|
||||
|
||||
Method method = repository.getMethod(methodName, paramTypes);
|
||||
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
|
||||
ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method,
|
||||
new DefaultRepositoryMetadata(repository), factory, mappingContextMock);
|
||||
|
||||
return new ReactiveMongoQueryFake(queryMethod, mongoOperationsMock);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReactiveMongoQueryFake extends AbstractReactiveMongoQuery {
|
||||
|
||||
private boolean isDeleteQuery;
|
||||
private boolean isLimitingQuery;
|
||||
|
||||
public ReactiveMongoQueryFake(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations) {
|
||||
super(method, operations);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Query createQuery(ConvertingParameterAccessor accessor) {
|
||||
return new BasicQuery("{'foo':'bar'}");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isCountQuery() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isExistsQuery() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isDeleteQuery() {
|
||||
return isDeleteQuery;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isLimiting() {
|
||||
return isLimitingQuery;
|
||||
}
|
||||
|
||||
public ReactiveMongoQueryFake setDeleteQuery(boolean isDeleteQuery) {
|
||||
this.isDeleteQuery = isDeleteQuery;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReactiveMongoQueryFake setLimitingQuery(boolean limitingQuery) {
|
||||
|
||||
isLimitingQuery = limitingQuery;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private interface Repo extends ReactiveMongoRepository<Person, Long> {
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(collation = "en_US")
|
||||
List<Person> findWithCollationUsingSpimpleStringValueByFirstName(String firstname);
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
|
||||
List<Person> findWithCollationUsingDocumentByFirstName(String firstname);
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(collation = "?1")
|
||||
List<Person> findWithCollationUsingPlaceholderByFirstName(String firstname, Object collation);
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : '?1' }")
|
||||
List<Person> findWithCollationUsingPlaceholderInDocumentByFirstName(String firstname, String collation);
|
||||
|
||||
List<Person> findWithCollationParameterByFirstName(String firstname, Collation collation);
|
||||
|
||||
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
|
||||
List<Person> findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation);
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,12 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.hamcrest.core.IsNull;
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.domain.Range.Bound;
|
||||
@@ -31,6 +28,7 @@ import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Metrics;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.core.query.TextCriteria;
|
||||
import org.springframework.data.mongodb.repository.Person;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
@@ -82,7 +80,7 @@ public class MongoParametersParameterAccessorUnitTests {
|
||||
|
||||
MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod,
|
||||
new Object[] { new Point(10, 20), DISTANCE });
|
||||
assertThat(accessor.getFullText(), IsNull.nullValue());
|
||||
assertThat(accessor.getFullText()).isNull();
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-973
|
||||
@@ -93,8 +91,8 @@ public class MongoParametersParameterAccessorUnitTests {
|
||||
|
||||
MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod,
|
||||
new Object[] { "spring", TextCriteria.forDefaultLanguage().matching("data") });
|
||||
assertThat(accessor.getFullText().getCriteriaObject().toJson(),
|
||||
equalTo(Document.parse("{ \"$text\" : { \"$search\" : \"data\"}}").toJson()));
|
||||
assertThat(accessor.getFullText().getCriteriaObject().toJson())
|
||||
.isEqualTo(Document.parse("{ \"$text\" : { \"$search\" : \"data\"}}").toJson());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1110
|
||||
@@ -111,8 +109,21 @@ public class MongoParametersParameterAccessorUnitTests {
|
||||
|
||||
Range<Distance> range = accessor.getDistanceRange();
|
||||
|
||||
assertThat(range.getLowerBound(), is(Bound.inclusive(min)));
|
||||
assertThat(range.getUpperBound(), is(Bound.inclusive(max)));
|
||||
assertThat(range.getLowerBound()).isEqualTo(Bound.inclusive(min));
|
||||
assertThat(range.getUpperBound()).isEqualTo(Bound.inclusive(max));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldDetectCollation() throws NoSuchMethodException, SecurityException {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Collation.class);
|
||||
MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
|
||||
|
||||
Collation collation = Collation.of("en_US");
|
||||
MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod,
|
||||
new Object[] { "dalinar", collation });
|
||||
|
||||
assertThat(accessor.getCollation()).isEqualTo(collation);
|
||||
}
|
||||
|
||||
interface PersonRepository extends Repository<Person, Long> {
|
||||
@@ -124,5 +135,8 @@ public class MongoParametersParameterAccessorUnitTests {
|
||||
List<Person> findByLocationNear(Point point, Range<Distance> distances);
|
||||
|
||||
List<Person> findByFirstname(String firstname, TextCriteria fullText);
|
||||
|
||||
List<Person> findByFirstname(String firstname, Collation collation);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.repository.query;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
@@ -29,6 +28,7 @@ import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.GeoResults;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.core.query.TextCriteria;
|
||||
import org.springframework.data.mongodb.repository.Near;
|
||||
import org.springframework.data.mongodb.repository.Person;
|
||||
@@ -47,17 +47,18 @@ public class MongoParametersUnitTests {
|
||||
|
||||
@Test
|
||||
public void discoversDistanceParameter() throws NoSuchMethodException, SecurityException {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class);
|
||||
MongoParameters parameters = new MongoParameters(method, false);
|
||||
|
||||
assertThat(parameters.getNumberOfParameters(), is(2));
|
||||
assertThat(parameters.getMaxDistanceIndex(), is(1));
|
||||
assertThat(parameters.getBindableParameters().getNumberOfParameters(), is(1));
|
||||
assertThat(parameters.getNumberOfParameters()).isEqualTo(2);
|
||||
assertThat(parameters.getMaxDistanceIndex()).isEqualTo(1);
|
||||
assertThat(parameters.getBindableParameters().getNumberOfParameters()).isOne();
|
||||
|
||||
Parameter parameter = parameters.getParameter(1);
|
||||
|
||||
assertThat(parameter.isSpecialParameter(), is(true));
|
||||
assertThat(parameter.isBindable(), is(false));
|
||||
assertThat(parameter.isSpecialParameter()).isTrue();
|
||||
assertThat(parameter.isBindable()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -65,39 +66,44 @@ public class MongoParametersUnitTests {
|
||||
Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class);
|
||||
MongoParameters parameters = new MongoParameters(method, false);
|
||||
|
||||
assertThat(parameters.getNearIndex(), is(-1));
|
||||
assertThat(parameters.getNearIndex()).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void rejectsMultiplePointsForGeoNearMethod() throws Exception {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("findByLocationNearAndOtherLocation", Point.class, Point.class);
|
||||
new MongoParameters(method, true);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void rejectsMultipleDoubleArraysForGeoNearMethod() throws Exception {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("invalidDoubleArrays", double[].class, double[].class);
|
||||
new MongoParameters(method, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotRejectMultiplePointsForSimpleQueryMethod() throws Exception {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("someOtherMethod", Point.class, Point.class);
|
||||
new MongoParameters(method, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findsAnnotatedPointForGeoNearQuery() throws Exception {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("findByOtherLocationAndLocationNear", Point.class, Point.class);
|
||||
MongoParameters parameters = new MongoParameters(method, true);
|
||||
assertThat(parameters.getNearIndex(), is(1));
|
||||
assertThat(parameters.getNearIndex()).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findsAnnotatedDoubleArrayForGeoNearQuery() throws Exception {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("validDoubleArrays", double[].class, double[].class);
|
||||
MongoParameters parameters = new MongoParameters(method, true);
|
||||
assertThat(parameters.getNearIndex(), is(1));
|
||||
assertThat(parameters.getNearIndex()).isOne();
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-973
|
||||
@@ -105,7 +111,7 @@ public class MongoParametersUnitTests {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("findByNameAndText", String.class, TextCriteria.class);
|
||||
MongoParameters parameters = new MongoParameters(method, false);
|
||||
assertThat(parameters.getFullTextParameterIndex(), is(1));
|
||||
assertThat(parameters.getFullTextParameterIndex()).isOne();
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-973
|
||||
@@ -113,7 +119,7 @@ public class MongoParametersUnitTests {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("findByNameAndText", String.class, TextCriteria.class);
|
||||
MongoParameters parameters = new MongoParameters(method, false);
|
||||
assertThat(parameters.getParameter(parameters.getFullTextParameterIndex()).isSpecialParameter(), is(true));
|
||||
assertThat(parameters.getParameter(parameters.getFullTextParameterIndex()).isSpecialParameter()).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1110
|
||||
@@ -122,19 +128,37 @@ public class MongoParametersUnitTests {
|
||||
Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Range.class);
|
||||
MongoParameters parameters = new MongoParameters(method, false);
|
||||
|
||||
assertThat(parameters.getRangeIndex(), is(1));
|
||||
assertThat(parameters.getMaxDistanceIndex(), is(-1));
|
||||
assertThat(parameters.getRangeIndex()).isOne();
|
||||
assertThat(parameters.getMaxDistanceIndex()).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1110
|
||||
public void shouldNotHaveMinDistanceIfOnlyOneDistanceParameterPresent() throws NoSuchMethodException,
|
||||
SecurityException {
|
||||
public void shouldNotHaveMinDistanceIfOnlyOneDistanceParameterPresent()
|
||||
throws NoSuchMethodException, SecurityException {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class);
|
||||
MongoParameters parameters = new MongoParameters(method, false);
|
||||
|
||||
assertThat(parameters.getRangeIndex(), is(-1));
|
||||
assertThat(parameters.getMaxDistanceIndex(), is(1));
|
||||
assertThat(parameters.getRangeIndex()).isEqualTo(-1);
|
||||
assertThat(parameters.getMaxDistanceIndex()).isOne();
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldReturnMinusOneIfCollationParameterDoesNotExist() throws NoSuchMethodException, SecurityException {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class);
|
||||
MongoParameters parameters = new MongoParameters(method, false);
|
||||
|
||||
assertThat(parameters.getCollationParameterIndex()).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void shouldReturnIndexOfCollationParameterIfExists() throws NoSuchMethodException, SecurityException {
|
||||
|
||||
Method method = PersonRepository.class.getMethod("findByText", String.class, Collation.class);
|
||||
MongoParameters parameters = new MongoParameters(method, false);
|
||||
|
||||
assertThat(parameters.getCollationParameterIndex()).isOne();
|
||||
}
|
||||
|
||||
interface PersonRepository {
|
||||
@@ -154,5 +178,7 @@ public class MongoParametersUnitTests {
|
||||
List<Person> findByNameAndText(String name, TextCriteria text);
|
||||
|
||||
List<Person> findByLocationNear(Point point, Range<Distance> range);
|
||||
|
||||
List<Person> findByText(String text, Collation collation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,10 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.mongodb.core.convert.MongoWriter;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.core.query.TextCriteria;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Simple {@link ParameterAccessor} that returns the given parameters unfiltered.
|
||||
@@ -40,6 +42,7 @@ class StubParameterAccessor implements MongoParameterAccessor {
|
||||
|
||||
private final Object[] values;
|
||||
private Range<Distance> range = Range.unbounded();
|
||||
private @Nullable Collation colllation;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ConvertingParameterAccessor} backed by a {@link StubParameterAccessor} simply returning the
|
||||
@@ -63,6 +66,8 @@ class StubParameterAccessor implements MongoParameterAccessor {
|
||||
this.range = (Range<Distance>) value;
|
||||
} else if (value instanceof Distance) {
|
||||
this.range = Range.from(Bound.<Distance> unbounded()).to(Bound.inclusive((Distance) value));
|
||||
} else if (value instanceof Collation) {
|
||||
this.colllation = Collation.class.cast(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,6 +138,15 @@ class StubParameterAccessor implements MongoParameterAccessor {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getCollation()
|
||||
*/
|
||||
@Override
|
||||
public Collation getCollation() {
|
||||
return this.colllation;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues()
|
||||
*/
|
||||
|
||||
@@ -15,18 +15,28 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.repository.support;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.anyString;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.mongodb.core.index.IndexDefinition;
|
||||
import org.springframework.data.mongodb.core.index.IndexOperations;
|
||||
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
|
||||
import org.springframework.data.mongodb.repository.query.MongoEntityMetadata;
|
||||
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
|
||||
import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
import org.springframework.data.util.Streamable;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link IndexEnsuringQueryCreationListener}.
|
||||
@@ -39,26 +49,109 @@ public class IndexEnsuringQueryCreationListenerUnitTests {
|
||||
IndexEnsuringQueryCreationListener listener;
|
||||
|
||||
@Mock IndexOperationsProvider provider;
|
||||
@Mock PartTree partTree;
|
||||
@Mock PartTreeMongoQuery partTreeQuery;
|
||||
@Mock MongoQueryMethod queryMethod;
|
||||
@Mock IndexOperations indexOperations;
|
||||
@Mock MongoEntityMetadata entityInformation;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
this.listener = new IndexEnsuringQueryCreationListener(provider);
|
||||
|
||||
partTreeQuery = mock(PartTreeMongoQuery.class, Answers.RETURNS_MOCKS);
|
||||
when(partTreeQuery.getTree()).thenReturn(partTree);
|
||||
when(provider.indexOps(anyString())).thenReturn(indexOperations);
|
||||
when(queryMethod.getEntityInformation()).thenReturn(entityInformation);
|
||||
when(entityInformation.getCollectionName()).thenReturn("persons");
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1753
|
||||
public void skipsQueryCreationForMethodWithoutPredicate() {
|
||||
|
||||
PartTree tree = mock(PartTree.class);
|
||||
when(tree.hasPredicate()).thenReturn(false);
|
||||
when(partTree.hasPredicate()).thenReturn(false);
|
||||
|
||||
PartTreeMongoQuery query = mock(PartTreeMongoQuery.class, Answers.RETURNS_MOCKS);
|
||||
when(query.getTree()).thenReturn(tree);
|
||||
|
||||
listener.onCreation(query);
|
||||
listener.onCreation(partTreeQuery);
|
||||
|
||||
verify(provider, times(0)).indexOps(any());
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void usesCollationWhenPresentAndFixedValue() {
|
||||
|
||||
when(partTree.hasPredicate()).thenReturn(true);
|
||||
when(partTree.getParts()).thenReturn(Streamable.empty());
|
||||
when(partTree.getSort()).thenReturn(Sort.unsorted());
|
||||
when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
|
||||
when(queryMethod.hasAnnotatedCollation()).thenReturn(true);
|
||||
when(queryMethod.getAnnotatedCollation()).thenReturn("en_US");
|
||||
|
||||
listener.onCreation(partTreeQuery);
|
||||
|
||||
ArgumentCaptor<IndexDefinition> indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
|
||||
verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
|
||||
|
||||
IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
|
||||
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new Document("collation", new Document("locale", "en_US")));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void usesCollationWhenPresentAndFixedDocumentValue() {
|
||||
|
||||
when(partTree.hasPredicate()).thenReturn(true);
|
||||
when(partTree.getParts()).thenReturn(Streamable.empty());
|
||||
when(partTree.getSort()).thenReturn(Sort.unsorted());
|
||||
when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
|
||||
when(queryMethod.hasAnnotatedCollation()).thenReturn(true);
|
||||
when(queryMethod.getAnnotatedCollation()).thenReturn("{ 'locale' : 'en_US' }");
|
||||
|
||||
listener.onCreation(partTreeQuery);
|
||||
|
||||
ArgumentCaptor<IndexDefinition> indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
|
||||
verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
|
||||
|
||||
IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
|
||||
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new Document("collation", new Document("locale", "en_US")));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void skipsCollationWhenPresentButDynamic() {
|
||||
|
||||
when(partTree.hasPredicate()).thenReturn(true);
|
||||
when(partTree.getParts()).thenReturn(Streamable.empty());
|
||||
when(partTree.getSort()).thenReturn(Sort.unsorted());
|
||||
when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
|
||||
when(queryMethod.hasAnnotatedCollation()).thenReturn(true);
|
||||
when(queryMethod.getAnnotatedCollation()).thenReturn("{ 'locale' : '?0' }");
|
||||
|
||||
listener.onCreation(partTreeQuery);
|
||||
|
||||
ArgumentCaptor<IndexDefinition> indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
|
||||
verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
|
||||
|
||||
IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
|
||||
assertThat(indexDefinition.getIndexOptions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1854
|
||||
public void skipsCollationWhenNotPresent() {
|
||||
|
||||
when(partTree.hasPredicate()).thenReturn(true);
|
||||
when(partTree.getParts()).thenReturn(Streamable.empty());
|
||||
when(partTree.getSort()).thenReturn(Sort.unsorted());
|
||||
when(partTreeQuery.getQueryMethod()).thenReturn(queryMethod);
|
||||
when(queryMethod.hasAnnotatedCollation()).thenReturn(false);
|
||||
|
||||
listener.onCreation(partTreeQuery);
|
||||
|
||||
ArgumentCaptor<IndexDefinition> indexArgumentCaptor = ArgumentCaptor.forClass(IndexDefinition.class);
|
||||
verify(indexOperations).ensureIndex(indexArgumentCaptor.capture());
|
||||
|
||||
IndexDefinition indexDefinition = indexArgumentCaptor.getValue();
|
||||
assertThat(indexDefinition.getIndexOptions()).isEmpty();
|
||||
}
|
||||
|
||||
interface SampleRepository {
|
||||
|
||||
Object findAllBy();
|
||||
|
||||
@@ -1738,6 +1738,223 @@ WARNING: Indexes are only used if the collation used for the operation matches t
|
||||
|
||||
include::./mongo-json-schema.adoc[leveloffset=+1]
|
||||
|
||||
<<mongo.repositories>> support `Collations` via the `@org.springframework.data.mongodb.repository.Query` annotation.
|
||||
|
||||
.Collation support for Repositories
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
public interface PersonRepository extends MongoRepository<Person, String> {
|
||||
|
||||
@Query(collation = "en_US") <1>
|
||||
List<Person> findByFirstname(String firstname);
|
||||
|
||||
@Query(collation = "{ 'locale' : 'en_US' }") <2>
|
||||
List<Person> findPersonByFirstname(String firstname);
|
||||
|
||||
@Query(collation = "?1") <3>
|
||||
List<Person> findByFirstname(String firstname, Object collation);
|
||||
|
||||
@Query(collation = "{ 'locale' : '?1' }") <4>
|
||||
List<Person> findByFirstname(String firstname, String collation);
|
||||
|
||||
List<Person> findByFirstname(String firstname, Collation collation); <5>
|
||||
|
||||
@Query(collation = "{ 'locale' : 'en_US' }")
|
||||
List<Person> findByFirstname(String firstname, @Nullable Collation collation); <6>
|
||||
}
|
||||
----
|
||||
<1> Static collation definition resulting in `{ 'locale' : 'en_US' }`.
|
||||
<2> Static collation definition resulting in `{ 'locale' : 'en_US' }`.
|
||||
<3> Dynamic collation depending on 2nd method argument. Allowed types include `String` (eg. 'en_US'), `Locacle` (eg. Locacle.US)
|
||||
and `Document` (eg. new Document("locale", "en_US"))
|
||||
<4> Dynamic collation depending on 2nd method argument.
|
||||
<5> Apply the `Collation` method parameter to the query.
|
||||
<6> The `Collation` method parameter overrides the default `collation` from `@Query` if not null.
|
||||
|
||||
NOTE: In case you enabled the automatic index creation for repository finder methods a potential static collation definition,
|
||||
as shown in (1) and (2), will be included when creating the index.
|
||||
====
|
||||
|
||||
[[mongo.jsonSchema]]
|
||||
=== JSON Schema
|
||||
|
||||
As of version 3.6, MongoDB supports collections that validate documents against a provided https://docs.mongodb.com/manual/core/schema-validation/#json-schema[JSON Schema].
|
||||
The schema itself and both validation action and level can be defined when creating the collection, as the following example shows:
|
||||
|
||||
.Sample JSON schema
|
||||
====
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"type": "object", <1>
|
||||
|
||||
"required": [ "firstname", "lastname" ], <2>
|
||||
|
||||
"properties": { <3>
|
||||
|
||||
"firstname": { <4>
|
||||
"type": "string",
|
||||
"enum": [ "luke", "han" ]
|
||||
},
|
||||
"address": { <5>
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"postCode": { "type": "string", "minLength": 4, "maxLength": 5 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> JSON schema documents always describe a whole document from its root. A schema is a schema object itself that can contain
|
||||
embedded schema objects that describe properties and subdocuments.
|
||||
<2> `required` is a property that describes which properties are required in a document. It can be specified optionally, along with other
|
||||
schema constraints. See MongoDB's documentation on https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#available-keywords[available keywords].
|
||||
<3> `properties` is related to a schema object that describes an `object` type. It contains property-specific schema constraints.
|
||||
<4> `firstname` specifies constraints for the `firsname` field inside the document. Here, it is a string-based `properties` element declaring
|
||||
possible field values.
|
||||
<5> `address` is a subdocument defining a schema for values in its `postCode` field.
|
||||
====
|
||||
|
||||
You can provide a schema either by specifying a schema document (that is, by using the `Document` API to parse or build a document object) or by building it with Spring Data's JSON schema utilities in `org.springframework.data.mongodb.core.schema`. `MongoJsonSchema` is the entry point for all JSON schema-related operations. The following example shows how use `MongoJsonSchema.builder()` to create a JSON schema:
|
||||
|
||||
.Creating a JSON schema
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
MongoJsonSchema.builder() <1>
|
||||
.required("firstname", "lastname") <2>
|
||||
|
||||
.properties(
|
||||
string("firstname").possibleValues("luke", "han"), <3>
|
||||
|
||||
object("address")
|
||||
.properties(string("postCode").minLength(4).maxLength(5)))
|
||||
|
||||
.build(); <4>
|
||||
----
|
||||
<1> Obtain a schema builder to configure the schema with a fluent API.
|
||||
<2> Configure required properties.
|
||||
<3> Configure the String-typed `firstname` field, allowing only `luke` and `han` values. Properties can be typed or untyped. Use a static import of `JsonSchemaProperty` to make the syntax slightly more compact and to get entry points such as `string(…)`.
|
||||
<4> Build the schema object. Use the schema to create either a collection or <<mongodb-template-query.criteria,query documents>>.
|
||||
====
|
||||
|
||||
There are already some predefined and strongly typed schema objects (`JsonSchemaObject` and `JsonSchemaProperty`) available
|
||||
through static methods on the gateway interfaces.
|
||||
However, you may need to build custom property validation rules, which can be created through the builder API, as the following example shows:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// "birthdate" : { "bsonType": "date" }
|
||||
JsonSchemaProperty.named("birthdate").ofType(Type.dateType());
|
||||
|
||||
// "birthdate" : { "bsonType": "date", "description", "Must be a date" }
|
||||
JsonSchemaProperty.named("birthdate").with(JsonSchemaObject.of(Type.dateType()).description("Must be a date"));
|
||||
----
|
||||
|
||||
`CollectionOptions` provides the entry point to schema support for collections, as the following example shows:
|
||||
|
||||
.Create collection with `$jsonSchema`
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname").build();
|
||||
|
||||
template.createCollection(Person.class, CollectionOptions.empty().schema(schema));
|
||||
----
|
||||
====
|
||||
|
||||
You can use a schema to query any collection for documents that match a given structure defined by a JSON schema, as the following example shows:
|
||||
|
||||
.Query for Documents matching a `$jsonSchema`
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname").build();
|
||||
|
||||
template.find(query(matchingDocumentStructure(schema)), Person.class);
|
||||
----
|
||||
====
|
||||
|
||||
The following table shows the supported JSON schema types:
|
||||
|
||||
[cols="3,1,6", options="header"]
|
||||
.Supported JSON schema types
|
||||
|===
|
||||
| Schema Type
|
||||
| Java Type
|
||||
| Schema Properties
|
||||
|
||||
| `untyped`
|
||||
| -
|
||||
| `description`, generated `description`, `enum`, `allOf`, `anyOf`, `oneOf`, `not`
|
||||
|
||||
| `object`
|
||||
| `Object`
|
||||
| `required`, `additionalProperties`, `properties`, `minProperties`, `maxProperties`, `patternProperties`
|
||||
|
||||
| `array`
|
||||
| any array except `byte[]`
|
||||
| `uniqueItems`, `additionalItems`, `items`, `minItems`, `maxItems`
|
||||
|
||||
| `string`
|
||||
| `String`
|
||||
| `minLength`, `maxLentgth`, `pattern`
|
||||
|
||||
| `int`
|
||||
| `int`, `Integer`
|
||||
| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
|
||||
|
||||
| `long`
|
||||
| `long`, `Long`
|
||||
| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
|
||||
|
||||
| `double`
|
||||
| `float`, `Float`, `double`, `Double`
|
||||
| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
|
||||
|
||||
| `decimal`
|
||||
| `BigDecimal`
|
||||
| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
|
||||
|
||||
| `number`
|
||||
| `Number`
|
||||
| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
|
||||
|
||||
| `binData`
|
||||
| `byte[]`
|
||||
| (none)
|
||||
|
||||
| `boolean`
|
||||
| `boolean`, `Boolean`
|
||||
| (none)
|
||||
|
||||
| `null`
|
||||
| `null`
|
||||
| (none)
|
||||
|
||||
| `objectId`
|
||||
| `ObjectId`
|
||||
| (none)
|
||||
|
||||
| `date`
|
||||
| `java.util.Date`
|
||||
| (none)
|
||||
|
||||
| `timestamp`
|
||||
| `BsonTimestamp`
|
||||
| (none)
|
||||
|
||||
| `regex`
|
||||
| `java.util.regex.Pattern`
|
||||
| (none)
|
||||
|
||||
|===
|
||||
|
||||
NOTE: `untyped` is a generic type that is inherited by all typed schema types. It provides all `untyped` schema properties to typed schema types.
|
||||
|
||||
For more information, see https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#op._S_jsonSchema[$jsonSchema].
|
||||
|
||||
[[mongo.query.fluent-template-api]]
|
||||
=== Fluent Template API
|
||||
|
||||
|
||||
Reference in New Issue
Block a user