Introduce dedicated Collation annotation.

The Collation annotation mainly serves as a meta annotation that allows common access to retrieving collation values for annotated queries, aggregations, etc.

Original Pull Request: #4131
This commit is contained in:
Christoph Strobl
2022-08-23 09:04:35 +02:00
parent 8aabf2fa5e
commit 0d752fd6e6
15 changed files with 317 additions and 54 deletions

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2022 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
*
* https://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.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* {@link Collation} allows to define the rules used for language-specific string comparison.
*
* @see <a href="https://www.mongodb.com/docs/manual/reference/collation/">https://www.mongodb.com/docs/manual/reference/collation/</a>
* @author Christoph Strobl
* @since 4.0
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface Collation {
/**
* The actual collation definition in JSON format or a
* {@link org.springframework.expression.spel.standard.SpelExpression template expression} resolving to either a JSON
* String or a {@link org.bson.Document}. The keys of the JSON document are configuration options for the collation.
*
* @return an empty {@link String} by default.
*/
String value() default "";
}

View File

@@ -0,0 +1,6 @@
/**
* Core Spring Data MongoDB annotations not limited to a special use case (like Query,...).
*/
@org.springframework.lang.NonNullApi
package org.springframework.data.mongodb.core.annotation;

View File

@@ -22,7 +22,10 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.mongodb.core.annotation.Collation;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* Mark a class to use compound indexes. <br />
* <p>
@@ -49,6 +52,7 @@ import org.springframework.data.mongodb.core.mapping.Document;
* @author Dave Perryman
* @author Stefan Tirea
*/
@Collation
@Target({ ElementType.TYPE })
@Documented
@Repeatable(CompoundIndexes.class)
@@ -181,5 +185,6 @@ public @interface CompoundIndex {
* "https://www.mongodb.com/docs/manual/reference/collation/">https://www.mongodb.com/docs/manual/reference/collation/</a>
* @since 4.0
*/
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default "";
}

View File

@@ -20,7 +20,10 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.mongodb.core.annotation.Collation;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* Mark a field to be indexed using MongoDB's indexing feature.
*
@@ -34,6 +37,7 @@ import org.springframework.data.mongodb.core.mapping.Document;
* @author Mark Paluch
* @author Stefan Tirea
*/
@Collation
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Indexed {
@@ -188,5 +192,6 @@ public @interface Indexed {
* @see <a href="https://www.mongodb.com/docs/manual/reference/collation/">https://www.mongodb.com/docs/manual/reference/collation/</a>
* @since 4.0
*/
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default "";
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.core.index;
import java.lang.annotation.Annotation;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@@ -23,13 +24,15 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.Association;
@@ -50,12 +53,10 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.mongodb.util.DotPath;
import org.springframework.data.mongodb.util.spel.ExpressionUtils;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -454,10 +455,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity));
}
if (StringUtils.hasText(index.collation())) {
indexDefinition.collation(evaluateCollation(index.collation(), entity));
}
indexDefinition.collation(resolveCollation(index, entity));
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
}
@@ -478,12 +476,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity));
}
if (StringUtils.hasText(index.collation())) {
indexDefinition.collation(evaluateCollation(index.collation(), entity));
} else if (entity != null && entity.hasCollation()) {
indexDefinition.collation(entity.getCollation());
}
indexDefinition.collation(resolveCollation(index, entity));
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
}
@@ -498,7 +491,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return new org.bson.Document(dotPath, 1);
}
Object keyDefToUse = evaluate(keyDefinitionString, getEvaluationContextForProperty(entity));
Object keyDefToUse = ExpressionUtils.evaluate(keyDefinitionString, () -> getEvaluationContextForProperty(entity));
org.bson.Document dbo = (keyDefToUse instanceof org.bson.Document) ? (org.bson.Document) keyDefToUse
: org.bson.Document.parse(ObjectUtils.nullSafeToString(keyDefToUse));
@@ -567,7 +560,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
Duration timeout = computeIndexTimeout(index.expireAfter(),
getEvaluationContextForProperty(persistentProperty.getOwner()));
() -> getEvaluationContextForProperty(persistentProperty.getOwner()));
if (!timeout.isZero() && !timeout.isNegative()) {
indexDefinition.expire(timeout);
}
@@ -577,16 +570,13 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), persistentProperty.getOwner()));
}
if (StringUtils.hasText(index.collation())) {
indexDefinition.collation(evaluateCollation(index.collation(), persistentProperty.getOwner()));
}
indexDefinition.collation(resolveCollation(index, persistentProperty.getOwner()));
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
}
private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity<?, ?> entity) {
Object result = evaluate(filterExpression, getEvaluationContextForProperty(entity));
Object result = ExpressionUtils.evaluate(filterExpression, () -> getEvaluationContextForProperty(entity));
if (result instanceof org.bson.Document) {
return PartialIndexFilter.of((org.bson.Document) result);
@@ -597,7 +587,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
private org.bson.Document evaluateWildcardProjection(String projectionExpression, PersistentEntity<?, ?> entity) {
Object result = evaluate(projectionExpression, getEvaluationContextForProperty(entity));
Object result = ExpressionUtils.evaluate(projectionExpression, () -> getEvaluationContextForProperty(entity));
if (result instanceof org.bson.Document) {
return (org.bson.Document) result;
@@ -608,7 +598,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
private Collation evaluateCollation(String collationExpression, PersistentEntity<?, ?> entity) {
Object result = evaluate(collationExpression, getEvaluationContextForProperty(entity));
Object result = ExpressionUtils.evaluate(collationExpression, () -> getEvaluationContextForProperty(entity));
if (result instanceof org.bson.Document) {
return Collation.from((org.bson.Document) result);
}
@@ -618,6 +608,9 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
if (result instanceof String) {
return Collation.parse(result.toString());
}
if (result instanceof Map) {
return Collation.from(new org.bson.Document((Map<String, ?>) result));
}
throw new IllegalStateException("Cannot parse collation " + result);
}
@@ -726,7 +719,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
String nameToUse = "";
if (StringUtils.hasText(indexName)) {
Object result = evaluate(indexName, getEvaluationContextForProperty(entity));
Object result = ExpressionUtils.evaluate(indexName, () -> getEvaluationContextForProperty(entity));
if (result != null) {
nameToUse = ObjectUtils.nullSafeToString(result);
@@ -787,9 +780,9 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
* @since 2.2
* @throws IllegalArgumentException for invalid duration values.
*/
private static Duration computeIndexTimeout(String timeoutValue, EvaluationContext evaluationContext) {
private static Duration computeIndexTimeout(String timeoutValue, Supplier<EvaluationContext> evaluationContext) {
Object evaluatedTimeout = evaluate(timeoutValue, evaluationContext);
Object evaluatedTimeout = ExpressionUtils.evaluate(timeoutValue, evaluationContext);
if (evaluatedTimeout == null) {
return Duration.ZERO;
@@ -808,15 +801,25 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return DurationStyle.detectAndParse(val);
}
/**
* Resolve the "collation" attribute from a given {@link Annotation} if present.
*
* @param annotation
* @param entity
* @return the collation present on either the annotation or the entity as a fallback. Might be {@literal null}.
* @since 4.0
*/
@Nullable
private static Object evaluate(String value, EvaluationContext evaluationContext) {
private Collation resolveCollation(Annotation annotation, @Nullable PersistentEntity<?, ?> entity) {
return MergedAnnotation.from(annotation).getValue("collation", String.class).filter(StringUtils::hasText)
.map(it -> evaluateCollation(it, entity)).orElseGet(() -> {
Expression expression = PARSER.parseExpression(value, ParserContext.TEMPLATE_EXPRESSION);
if (expression instanceof LiteralExpression) {
return value;
}
return expression.getValue(evaluationContext, Object.class);
if (entity instanceof MongoPersistentEntity<?> mongoPersistentEntity
&& mongoPersistentEntity.hasCollation()) {
return mongoPersistentEntity.getCollation();
}
return null;
});
}
private static boolean isMapWithoutWildcardIndex(MongoPersistentProperty property) {

View File

@@ -21,6 +21,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.mongodb.core.annotation.Collation;
/**
* Annotation for an entity or property that should be used as key for a
* <a href="https://docs.mongodb.com/manual/core/index-wildcard/">Wildcard Index</a>. <br />
@@ -79,6 +82,7 @@ import java.lang.annotation.Target;
* @author Christoph Strobl
* @since 3.3
*/
@Collation
@Documented
@Target({ ElementType.TYPE, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@@ -126,5 +130,6 @@ public @interface WildcardIndexed {
*
* @return an empty {@link String} by default.
*/
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default "";
}

View File

@@ -23,6 +23,7 @@ import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.mongodb.core.annotation.Collation;
/**
* Identifies a domain object to be persisted to MongoDB.
@@ -32,6 +33,7 @@ import org.springframework.data.annotation.Persistent;
* @author Christoph Strobl
*/
@Persistent
@Collation
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@@ -71,6 +73,7 @@ public @interface Document {
* @return an empty {@link String} by default.
* @since 2.2
*/
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default "";
}

View File

@@ -23,6 +23,7 @@ import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.annotation.QueryAnnotation;
import org.springframework.data.mongodb.core.annotation.Collation;
/**
* The {@link Aggregation} annotation can be used to annotate a {@link org.springframework.data.repository.Repository}
@@ -38,6 +39,7 @@ import org.springframework.data.annotation.QueryAnnotation;
* @author Christoph Strobl
* @since 2.2
*/
@Collation
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
@@ -123,5 +125,6 @@ public @interface Aggregation {
*
* @return an empty {@link String} by default.
*/
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default "";
}

View File

@@ -21,7 +21,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.annotation.QueryAnnotation;
import org.springframework.data.mongodb.core.annotation.Collation;
/**
* Annotation to declare finder queries directly on repository methods. Both attributes allow using a placeholder
@@ -32,6 +34,7 @@ import org.springframework.data.annotation.QueryAnnotation;
* @author Christoph Strobl
* @author Mark Paluch
*/
@Collation
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
@@ -124,5 +127,6 @@ public @interface Query {
* @return an empty {@link String} by default.
* @since 2.2
*/
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default "";
}

View File

@@ -28,6 +28,7 @@ import org.springframework.data.geo.GeoPage;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.annotation.Collation;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
@@ -321,14 +322,7 @@ public class MongoQueryMethod extends QueryMethod {
* @since 2.2
*/
public boolean hasAnnotatedCollation() {
Optional<String> optionalCollation = lookupQueryAnnotation().map(Query::collation);
if (!optionalCollation.isPresent()) {
optionalCollation = lookupAggregationAnnotation().map(Aggregation::collation);
}
return optionalCollation.filter(StringUtils::hasText).isPresent();
return doFindAnnotation(Collation.class).map(Collation::value).filter(StringUtils::hasText).isPresent();
}
/**
@@ -341,10 +335,9 @@ public class MongoQueryMethod extends QueryMethod {
*/
public String getAnnotatedCollation() {
return lookupQueryAnnotation().map(Query::collation)
.orElseGet(() -> lookupAggregationAnnotation().map(Aggregation::collation) //
return doFindAnnotation(Collation.class).map(Collation::value) //
.orElseThrow(() -> new IllegalStateException(
"Expected to find @Query annotation but did not; Make sure to check hasAnnotatedCollation() before.")));
"Expected to find @Collation annotation but did not; Make sure to check hasAnnotatedCollation() before."));
}
/**
@@ -447,7 +440,7 @@ public class MongoQueryMethod extends QueryMethod {
private boolean isNumericOrVoidReturnValue() {
Class<?> resultType = getReturnedObjectType();
if(ReactiveWrappers.usesReactiveType(resultType)) {
if (ReactiveWrappers.usesReactiveType(resultType)) {
resultType = getReturnType().getComponentType().getType();
}

View File

@@ -15,6 +15,9 @@
*/
package org.springframework.data.mongodb.util.spel;
import java.util.function.Supplier;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
@@ -49,4 +52,15 @@ public final class ExpressionUtils {
Expression expression = PARSER.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION);
return expression instanceof LiteralExpression ? null : expression;
}
@Nullable
public static Object evaluate(String value, Supplier<EvaluationContext> evaluationContext) {
Expression expression = detectExpression(value);
if (expression == null) {
return value;
}
return expression.getValue(evaluationContext.get(), Object.class);
}
}

View File

@@ -713,6 +713,32 @@ public class MongoPersistentEntityIndexResolverUnitTests {
assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1));
}
@Test // GH-3002
public void compoundIndexWithCollationFromDocumentAnnotation() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
WithCompoundCollationFromDocument.class);
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
assertThat(indexDefinition.getIndexOptions())
.isEqualTo(new org.bson.Document().append("name", "compound_index_with_collation").append("collation",
new org.bson.Document().append("locale", "en_US").append("strength", 2)));
assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1));
}
@Test // GH-3002
public void compoundIndexWithEvaluatedCollationFromAnnotation() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
WithEvaluatedCollationFromCompoundIndex.class);
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
assertThat(indexDefinition.getIndexOptions())
.isEqualTo(new org.bson.Document().append("name", "compound_index_with_collation").append("collation",
new org.bson.Document().append("locale", "de_AT")));
assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1));
}
@Document("CompoundIndexOnLevelOne")
class CompoundIndexOnLevelOne {
@@ -793,6 +819,14 @@ public class MongoPersistentEntityIndexResolverUnitTests {
@CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}",
collation = "{'locale': 'en_US', 'strength': 2}")
class CompoundIndexWithCollation {}
@Document(collation = "{'locale': 'en_US', 'strength': 2}")
@CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}")
class WithCompoundCollationFromDocument {}
@Document(collation = "{'locale': 'en_US', 'strength': 2}")
@CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}", collation = "#{{ 'locale' : 'de' + '_' + 'AT' }}")
class WithEvaluatedCollationFromCompoundIndex {}
}
public static class TextIndexedResolutionTests {
@@ -1423,7 +1457,7 @@ public class MongoPersistentEntityIndexResolverUnitTests {
public void indexedWithCollation() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
IndexedWithCollation.class);
WithCollationFromIndexedAnnotation.class);
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value")
@@ -1431,6 +1465,29 @@ public class MongoPersistentEntityIndexResolverUnitTests {
.append("collation", new org.bson.Document().append("locale", "en_US").append("strength", 2)));
}
@Test // GH-3002
public void indexedWithCollationFromDocumentAnnotation() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
WithCollationFromDocumentAnnotation.class);
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value")
.append("unique", true)
.append("collation", new org.bson.Document().append("locale", "en_US").append("strength", 2)));
}
@Test // GH-3002
public void indexedWithEvaluatedCollation() {
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
WithEvaluatedCollationFromIndexedAnnotation.class);
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value")
.append("collation", new org.bson.Document().append("locale", "de_AT")));
}
@Document
class MixedIndexRoot {
@@ -1749,11 +1806,26 @@ public class MongoPersistentEntityIndexResolverUnitTests {
}
@Document
class IndexedWithCollation {
class WithCollationFromIndexedAnnotation {
@Indexed(collation = "{'locale': 'en_US', 'strength': 2}", unique = true) //
private String value;
}
@Document(collation = "{'locale': 'en_US', 'strength': 2}")
class WithCollationFromDocumentAnnotation {
@Indexed(unique = true) //
private String value;
}
@Document(collation = "en_US")
class WithEvaluatedCollationFromIndexedAnnotation {
@Indexed(collation = "#{{'locale' : 'de' + '_' + 'AT'}}") //
private String value;
}
@HashIndexed
@Indexed
@Retention(RetentionPolicy.RUNTIME)

View File

@@ -31,6 +31,7 @@ import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.User;
import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
import org.springframework.data.mongodb.core.annotation.Collation;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
@@ -39,6 +40,7 @@ import org.springframework.data.mongodb.repository.Aggregation;
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.mongodb.repository.Query;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
@@ -278,6 +280,33 @@ public class MongoQueryMethodUnitTests {
.withMessageContaining("findAndIncrementVisitsByFirstname");
}
@Test // GH-3002
void readsCollationFromAtCollationAnnotation() throws Exception {
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithCollationFromAtCollationByFirstname", String.class);
assertThat(method.hasAnnotatedCollation()).isTrue();
assertThat(method.getAnnotatedCollation()).isEqualTo("en_US");
}
@Test // GH-3002
void readsCollationFromAtQueryAnnotation() throws Exception {
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithCollationFromAtQueryByFirstname", String.class);
assertThat(method.hasAnnotatedCollation()).isTrue();
assertThat(method.getAnnotatedCollation()).isEqualTo("en_US");
}
@Test // GH-3002
void annotatedCollationClashSelectsAtCollationAnnotationValue() throws Exception {
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname", String.class);
assertThat(method.hasAnnotatedCollation()).isTrue();
assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT");
}
private MongoQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters) throws Exception {
Method method = repository.getMethod(name, parameters);
@@ -338,6 +367,16 @@ public class MongoQueryMethodUnitTests {
void findAndUpdateBy(String firstname, UpdateDefinition update);
void findAndUpdateBy(String firstname, AggregationUpdate update);
@Collation("en_US")
List<User> findWithCollationFromAtCollationByFirstname(String firstname);
@Query(collation = "en_US")
List<User> findWithCollationFromAtQueryByFirstname(String firstname);
@Collation("de_AT")
@Query(collation = "en_US")
List<User> findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname);
}
interface SampleRepository extends Repository<Contact, Long> {

View File

@@ -17,6 +17,10 @@ package org.springframework.data.mongodb.repository.query;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.mongodb.core.annotation.Collation;
import org.springframework.data.mongodb.repository.Query;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -24,8 +28,6 @@ import java.lang.reflect.Method;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Page;
@@ -56,7 +58,7 @@ public class ReactiveMongoQueryMethodUnitTests {
MongoMappingContext context;
@Before
@BeforeEach
public void setUp() {
context = new MongoMappingContext();
}
@@ -102,13 +104,13 @@ public class ReactiveMongoQueryMethodUnitTests {
.isTrue();
}
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1444
@Test // DATAMONGO-1444
public void rejectsNullMappingContext() throws Exception {
Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Point.class);
new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class),
new SpelAwareProxyProjectionFactory(), null);
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new MongoQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class),
new SpelAwareProxyProjectionFactory(), null));
}
@Test // DATAMONGO-1444
@@ -197,6 +199,33 @@ public class ReactiveMongoQueryMethodUnitTests {
.withMessageContaining("findAndIncrementVisitsByFirstname");
}
@Test // GH-3002
void readsCollationFromAtCollationAnnotation() throws Exception {
ReactiveMongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithCollationFromAtCollationByFirstname", String.class);
assertThat(method.hasAnnotatedCollation()).isTrue();
assertThat(method.getAnnotatedCollation()).isEqualTo("en_US");
}
@Test // GH-3002
void readsCollationFromAtQueryAnnotation() throws Exception {
ReactiveMongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithCollationFromAtQueryByFirstname", String.class);
assertThat(method.hasAnnotatedCollation()).isTrue();
assertThat(method.getAnnotatedCollation()).isEqualTo("en_US");
}
@Test // GH-3002
void annotatedCollationClashSelectsAtCollationAnnotationValue() throws Exception {
ReactiveMongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname", String.class);
assertThat(method.hasAnnotatedCollation()).isTrue();
assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT");
}
private ReactiveMongoQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters)
throws Exception {
@@ -238,6 +267,16 @@ public class ReactiveMongoQueryMethodUnitTests {
@Aggregation(pipeline = "{'$group': { _id: '$templateId', maxVersion : { $max : '$version'} } }",
collation = "de_AT")
Flux<User> findByAggregationWithCollation();
@Collation("en_US")
List<User> findWithCollationFromAtCollationByFirstname(String firstname);
@Query(collation = "en_US")
List<User> findWithCollationFromAtQueryByFirstname(String firstname);
@Collation("de_AT")
@Query(collation = "en_US")
List<User> findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname);
}
interface SampleRepository extends Repository<Contact, Long> {

View File

@@ -2011,7 +2011,35 @@ and `Document` (eg. new Document("locale", "en_US"))
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.
TIP: The most specifc `Collation` outroules potentially defined others. Which means Method argument over query method annotation over doamin type annotation.
TIP: The most specifc `Collation` outrules potentially defined others. Which means Method argument over query method annotation over domain type annotation.
====
To streamline usage of collation attributes throughout the codebase it is also possible to use the `@Collation` annotation, which serves as a meta annotation for the ones mentioned above.
The same rules and locations apply, plus, direct usage of `@Collation` supersedes any collation values defined on `@Query` and other annotations.
Which means, if a collation is declared via `@Query` and additionally via `@Collation`, then the one from `@Collation` is picked.
.Using `@Collation`
====
[source,java]
----
@Collation("en_US") <1>
class Game {
// ...
}
interface GameRepository extends Repository<Game, String> {
@Collation("en_GB") <2>
List<Game> findByTitle(String title);
@Collation("de_AT") <3>
@Query(collation="en_GB")
List<Game> findByDescriptionContaining(String keyword);
}
----
<1> Instead of `@Document(collation=...)`.
<2> Instead of `@Query(collation=...)`.
<3> Favors `@Collation` over meta usage.
====
include::./mongo-json-schema.adoc[leveloffset=+1]