From 7b44f78133459583fb0c693c173f97ee431a6e44 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 21 Mar 2023 12:49:34 +0100 Subject: [PATCH] Add Hint annotation. This commit introduces the new `@Hint` annotation that allows to override MongoDB's default index selection for repository query, update and aggregate operations. ``` @Hint("lastname-idx") List findByLastname(String lastname); @Query(value = "{ 'firstname' : ?0 }", hint="firstname-idx") List findByFirstname(String firstname); ``` Closes: #3230 Original pull request: #4339 --- .../data/mongodb/repository/Hint.java | 48 +++++++++++++++++ .../data/mongodb/repository/Query.java | 22 +++++++- .../repository/query/AbstractMongoQuery.java | 16 ++++++ .../query/AbstractReactiveMongoQuery.java | 16 ++++++ .../repository/query/AggregationUtils.java | 15 ++++++ .../repository/query/MongoQueryMethod.java | 22 ++++++++ .../query/ReactiveStringBasedAggregation.java | 1 + .../query/StringBasedAggregation.java | 2 + .../query/AbstractMongoQueryUnitTests.java | 30 ++++++++++- .../AbstractReactiveMongoQueryUnitTests.java | 52 +++++++++++++++++++ ...activeStringBasedAggregationUnitTests.java | 18 +++++++ .../StringBasedAggregationUnitTests.java | 18 +++++++ .../reference/mongo-repositories.adoc | 19 +++++++ 13 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Hint.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Hint.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Hint.java new file mode 100644 index 000000000..480b8085e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Hint.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 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.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to declare index hints for repository query, update and aggregate operations. The index is specified by + * its name. + * + * @author Christoph Strobl + * @since 4.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +public @interface Hint { + + String value() default ""; + + /** + * The name of the index to use. In case of an {@literal aggregation} the index is evaluated against the initial + * collection or view. Specify the index either by the index name. + * + * @return the index name. + */ + @AliasFor("value") + String indexName() default ""; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java index 9f0031e79..18f8b269b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java @@ -39,13 +39,14 @@ import org.springframework.data.mongodb.core.annotation.Collation; @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Documented @QueryAnnotation +@Hint public @interface Query { /** * Takes a MongoDB JSON string to define the actual query to be executed. This one will take precedence over the * method name then. * - * @return empty {@link String} by default. + * @return empty {@link String} by default. */ String value() default ""; @@ -53,7 +54,7 @@ public @interface Query { * Defines the fields that should be returned for the given query. Note that only these fields will make it into the * domain object returned. * - * @return empty {@link String} by default. + * @return empty {@link String} by default. */ String fields() default ""; @@ -129,4 +130,21 @@ public @interface Query { */ @AliasFor(annotation = Collation.class, attribute = "value") String collation() default ""; + + /** + * The name of the index to use.
+ * {@code @Query(value = "...", hint = "lastname-idx")} can be used as shortcut for: + * + *
+	 * @Query(...)
+	 * @Hint("lastname-idx")
+	 * List<User> findAllByLastname(String collation);
+	 * 
+ * + * @return the index name. + * @since 4.1 + * @see Hint#indexName() + */ + @AliasFor(annotation = Hint.class, attribute = "indexName") + String hint() default ""; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index 930a73331..e4a533b7d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -135,6 +135,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { applyQueryMetaAttributesWhenPresent(query); query = applyAnnotatedDefaultSortIfPresent(query); query = applyAnnotatedCollationIfPresent(query, accessor); + query = applyHintIfPresent(query); FindWithQuery find = typeToRead == null // ? executableFind // @@ -225,6 +226,21 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { accessor, getQueryMethod().getParameters(), expressionParser, evaluationContextProvider); } + /** + * If present apply the hint from the {@link org.springframework.data.mongodb.repository.Hint} annotation. + * + * @param query must not be {@literal null}. + * @return never {@literal null}. + * @since 4.1 + */ + Query applyHintIfPresent(Query query) { + + if(!method.hasAnnotatedHint()) { + return query; + } + return query.withHint(method.getAnnotatedHint()); + } + /** * 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 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java index fbb078b43..85f92d918 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java @@ -160,6 +160,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { applyQueryMetaAttributesWhenPresent(query); query = applyAnnotatedDefaultSortIfPresent(query); query = applyAnnotatedCollationIfPresent(query, accessor); + query = applyHintIfPresent(query); FindWithQuery find = typeToRead == null // ? findOperationWithProjection // @@ -269,6 +270,21 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { accessor, getQueryMethod().getParameters(), expressionParser, evaluationContextProvider); } + /** + * If present apply the hint from the {@link org.springframework.data.mongodb.repository.Hint} annotation. + * + * @param query must not be {@literal null}. + * @return never {@literal null}. + * @since 4.1 + */ + Query applyHintIfPresent(Query query) { + + if(!method.hasAnnotatedHint()) { + return query; + } + return query.withHint(method.getAnnotatedHint()); + } + /** * 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 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java index 0ee488c33..d88655887 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java @@ -102,6 +102,21 @@ abstract class AggregationUtils { return builder; } + /** + * If present apply the hint from the {@link org.springframework.data.mongodb.repository.Hint} annotation. + * + * @param builder must not be {@literal null}. + * @return never {@literal null}. + * @since 4.1 + */ + static AggregationOptions.Builder applyHint(AggregationOptions.Builder builder, MongoQueryMethod queryMethod) { + + if(!queryMethod.hasAnnotatedHint()) { + return builder; + } + return builder.hint(queryMethod.getAnnotatedHint()); + } + /** * Append {@code $sort} aggregation stage if {@link ConvertingParameterAccessor#getSort()} is present. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java index 1b8f6b6a5..8db045881 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java @@ -33,6 +33,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.Aggregation; +import org.springframework.data.mongodb.repository.Hint; import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.Tailable; @@ -362,6 +363,27 @@ public class MongoQueryMethod extends QueryMethod { "Expected to find @Aggregation annotation but did not; Make sure to check hasAnnotatedAggregation() before.")); } + /** + * @return {@literal true} if the {@link Hint} annotation is present and the index name is not empty. + * @since 4.1 + */ + public boolean hasAnnotatedHint() { + return StringUtils.hasText(getAnnotatedHint()); + } + + /** + * Returns the aggregation pipeline declared via a {@link Hint} annotation. + * + * @return the index name (might be empty) or {@literal null} if not present. + * @since 4.1 + */ + @Nullable + public String getAnnotatedHint() { + + Optional hint = doFindAnnotation(Hint.class); + return hint.map(Hint::indexName).orElse(null); + } + private Optional findAnnotatedAggregation() { return lookupAggregationAnnotation() // diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java index 13c49b521..1ebfdbfcf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java @@ -128,6 +128,7 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery { AggregationUtils.applyCollation(builder, method.getAnnotatedCollation(), accessor, method.getParameters(), expressionParser, evaluationContextProvider); AggregationUtils.applyMeta(builder, method); + AggregationUtils.applyHint(builder, method); return builder.build(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java index a0ee58fd2..a3847a390 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java @@ -29,6 +29,7 @@ import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperation; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; +import org.springframework.data.mongodb.core.aggregation.AggregationOptions.Builder; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.convert.MongoConverter; @@ -178,6 +179,7 @@ public class StringBasedAggregation extends AbstractMongoQuery { AggregationUtils.applyCollation(builder, method.getAnnotatedCollation(), accessor, method.getParameters(), expressionParser, evaluationContextProvider); AggregationUtils.applyMeta(builder, method); + AggregationUtils.applyHint(builder, method); return builder.build(); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java index 304bfadb9..a6c7d3ed9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java @@ -61,6 +61,7 @@ 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.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.Hint; import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Update; @@ -458,7 +459,7 @@ class AbstractMongoQueryUnitTests { void updateExecutionCallsUpdateAllCorrectly() { when(terminatingUpdate.all()).thenReturn(updateResultMock); - + createQueryForMethod("findAndIncreaseVisitsByLastname", String.class, int.class) // .execute(new Object[] { "dalinar", 100 }); @@ -469,6 +470,29 @@ class AbstractMongoQueryUnitTests { assertThat(update.getValue().getUpdateObject()).isEqualTo(Document.parse("{ '$inc' : { 'visits' : 100 } }")); } + @Test // GH-3230 + void findShouldApplyHint() { + + createQueryForMethod("findWithHintByFirstname", String.class).execute(new Object[] { "Jasna" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getHint()).isEqualTo("idx-fn"); + } + + @Test // GH-3230 + void updateShouldApplyHint() { + + when(terminatingUpdate.all()).thenReturn(updateResultMock); + + createQueryForMethod("findAndIncreaseVisitsByLastname", String.class, int.class) // + .execute(new Object[] { "dalinar", 100 }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(executableUpdate).matching(captor.capture()); + assertThat(captor.getValue().getHint()).isEqualTo("idx-ln"); + } + private MongoQueryFake createQueryForMethod(String methodName, Class... paramTypes) { return createQueryForMethod(Repo.class, methodName, paramTypes); } @@ -584,8 +608,12 @@ class AbstractMongoQueryUnitTests { @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }") List findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation); + @Hint("idx-ln") @Update("{ '$inc' : { 'visits' : ?1 } }") void findAndIncreaseVisitsByLastname(String lastname, int value); + + @Hint("idx-fn") + void findWithHintByFirstname(String firstname); } // DATAMONGO-1872 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java index e74f841e1..e7fc85400 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java @@ -18,6 +18,15 @@ package org.springframework.data.mongodb.repository.query; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.result.UpdateResult; +import org.bson.codecs.configuration.CodecRegistry; +import org.springframework.data.mongodb.core.ReactiveUpdateOperation.TerminatingUpdate; +import org.springframework.data.mongodb.core.ReactiveUpdateOperation.ReactiveUpdate; +import org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithQuery; +import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.Hint; +import org.springframework.data.mongodb.repository.Update; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -71,6 +80,9 @@ class AbstractReactiveMongoQueryUnitTests { @Mock ReactiveFind executableFind; @Mock FindWithQuery withQueryMock; + @Mock ReactiveUpdate executableUpdate; + @Mock UpdateWithQuery updateWithQuery; + @Mock TerminatingUpdate terminatingUpdate; @BeforeEach void setUp() { @@ -91,6 +103,11 @@ class AbstractReactiveMongoQueryUnitTests { doReturn(Flux.empty()).when(withQueryMock).all(); doReturn(Mono.empty()).when(withQueryMock).first(); doReturn(Mono.empty()).when(withQueryMock).one(); + + doReturn(executableUpdate).when(mongoOperationsMock).update(any()); + doReturn(executableUpdate).when(executableUpdate).inCollection(anyString()); + doReturn(updateWithQuery).when(executableUpdate).matching(any(Query.class)); + doReturn(terminatingUpdate).when(updateWithQuery).apply(any(UpdateDefinition.class)); } @Test // DATAMONGO-1854 @@ -223,6 +240,29 @@ class AbstractReactiveMongoQueryUnitTests { .contains(Collation.of("en_US").toDocument()); } + @Test // GH-3230 + void findShouldApplyHint() { + + createQueryForMethod("findWithHintByFirstname", String.class).executeBlocking(new Object[] { "Jasna" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getHint()).isEqualTo("idx-fn"); + } + + @Test // GH-3230 + void updateShouldApplyHint() { + + when(terminatingUpdate.all()).thenReturn(Mono.just(mock(UpdateResult.class))); + + createQueryForMethod("findAndIncreaseVisitsByLastname", String.class, int.class) // + .executeBlocking(new Object[] { "dalinar", 100 }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(executableUpdate).matching(captor.capture()); + assertThat(captor.getValue().getHint()).isEqualTo("idx-ln"); + } + private ReactiveMongoQueryFake createQueryForMethod(String methodName, Class... paramTypes) { return createQueryForMethod(Repo.class, methodName, paramTypes); } @@ -291,6 +331,11 @@ class AbstractReactiveMongoQueryUnitTests { isLimitingQuery = limitingQuery; return this; } + + @Override + protected Mono getCodecRegistry() { + return Mono.just(MongoClientSettings.getDefaultCodecRegistry()); + } } private interface Repo extends ReactiveMongoRepository { @@ -315,5 +360,12 @@ class AbstractReactiveMongoQueryUnitTests { @org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }") List findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation); + + @Hint("idx-ln") + @Update("{ '$inc' : { 'visits' : ?1 } }") + void findAndIncreaseVisitsByLastname(String lastname, int value); + + @Hint("idx-fn") + void findWithHintByFirstname(String firstname); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java index 8b4645765..99b0b241d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.*; import lombok.Value; import org.reactivestreams.Publisher; +import org.springframework.data.mongodb.repository.Hint; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -173,6 +174,13 @@ public class ReactiveStringBasedAggregationUnitTests { verify(operations).execute(any()); } + @Test // GH-3230 + void aggregatePicksUpHintFromAnnotation() { + + AggregationInvocation invocation = executeAggregation("withHint"); + assertThat(hintOf(invocation)).isEqualTo("idx"); + } + private AggregationInvocation executeAggregation(String name, Object... args) { Class[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(size -> new Class[size]); @@ -216,6 +224,12 @@ public class ReactiveStringBasedAggregationUnitTests { : null; } + @Nullable + private Object hintOf(AggregationInvocation invocation) { + return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getHintObject().orElse(null) + : null; + } + private Class targetTypeOf(AggregationInvocation invocation) { return invocation.getTargetType(); } @@ -243,6 +257,10 @@ public class ReactiveStringBasedAggregationUnitTests { @Aggregation(pipeline = RAW_GROUP_BY_LASTNAME_STRING, collation = "de_AT") Mono aggregateWithCollation(Collation collation); + + @Hint("idx") + @Aggregation(RAW_GROUP_BY_LASTNAME_STRING) + String withHint(); } static class PersonAggregate { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java index 62c699e92..a88534afd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java @@ -58,6 +58,7 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.repository.Aggregation; +import org.springframework.data.mongodb.repository.Hint; import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.projection.ProjectionFactory; @@ -260,6 +261,13 @@ public class StringBasedAggregationUnitTests { .withMessageContaining("Page"); } + @Test // GH-3230 + void aggregatePicksUpHintFromAnnotation() { + + AggregationInvocation invocation = executeAggregation("withHint"); + assertThat(hintOf(invocation)).isEqualTo("idx"); + } + private AggregationInvocation executeAggregation(String name, Object... args) { Class[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new); @@ -302,6 +310,12 @@ public class StringBasedAggregationUnitTests { : null; } + @Nullable + private Object hintOf(AggregationInvocation invocation) { + return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getHintObject().orElse(null) + : null; + } + private Class targetTypeOf(AggregationInvocation invocation) { return invocation.getTargetType(); } @@ -350,6 +364,10 @@ public class StringBasedAggregationUnitTests { @Aggregation(RAW_GROUP_BY_LASTNAME_STRING) String simpleReturnType(); + + @Hint("idx") + @Aggregation(RAW_GROUP_BY_LASTNAME_STRING) + String withHint(); } private interface UnsupportedRepository extends Repository { diff --git a/src/main/asciidoc/reference/mongo-repositories.adoc b/src/main/asciidoc/reference/mongo-repositories.adoc index de46bdcdb..4090c8b8b 100644 --- a/src/main/asciidoc/reference/mongo-repositories.adoc +++ b/src/main/asciidoc/reference/mongo-repositories.adoc @@ -297,6 +297,25 @@ lower / upper bounds (`$gt` / `$gte` & `$lt` / `$lte`) according to `Range` NOTE: If the property criterion compares a document, the order of the fields and exact equality in the document matters. +[[mongodb.repositories.queries.hint]] +=== Repository Index Hints + +The `@Hint` annotation allows to override MongoDB's default index selection and forces the database to use the specified index instead. + +.Example of index hints +==== +[source,java] +---- +@Hint("lastname-idx") <1> +List findByLastname(String lastname); + +@Query(value = "{ 'firstname' : ?0 }", hint="firstname-idx") <2> +List findByFirstname(String firstname); +---- +<1> Use the index with name `lastname-idx`. +<2> The `@Query` annotation defines the `hint` alias which is equivalent to explicitly adding the `@Hint` annotation. +==== + [[mongodb.repositories.queries.update]] === Repository Update Methods