Skip to content

Commit 7b44f78

Browse files
christophstroblmp911de
authored andcommitted
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<Person> findByLastname(String lastname); @query(value = "{ 'firstname' : ?0 }", hint="firstname-idx") List<Person> findByFirstname(String firstname); ``` Closes: #3230 Original pull request: #4339
1 parent af2076d commit 7b44f78

File tree

13 files changed

+276
-3
lines changed

13 files changed

+276
-3
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.repository;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
import org.springframework.core.annotation.AliasFor;
25+
26+
/**
27+
* Annotation to declare index hints for repository query, update and aggregate operations. The index is specified by
28+
* its name.
29+
*
30+
* @author Christoph Strobl
31+
* @since 4.1
32+
*/
33+
@Retention(RetentionPolicy.RUNTIME)
34+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
35+
@Documented
36+
public @interface Hint {
37+
38+
String value() default "";
39+
40+
/**
41+
* The name of the index to use. In case of an {@literal aggregation} the index is evaluated against the initial
42+
* collection or view. Specify the index either by the index name.
43+
*
44+
* @return the index name.
45+
*/
46+
@AliasFor("value")
47+
String indexName() default "";
48+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,22 @@
3939
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
4040
@Documented
4141
@QueryAnnotation
42+
@Hint
4243
public @interface Query {
4344

4445
/**
4546
* Takes a MongoDB JSON string to define the actual query to be executed. This one will take precedence over the
4647
* method name then.
4748
*
48-
* @return empty {@link String} by default.
49+
* @return empty {@link String} by default.
4950
*/
5051
String value() default "";
5152

5253
/**
5354
* Defines the fields that should be returned for the given query. Note that only these fields will make it into the
5455
* domain object returned.
5556
*
56-
* @return empty {@link String} by default.
57+
* @return empty {@link String} by default.
5758
*/
5859
String fields() default "";
5960

@@ -129,4 +130,21 @@
129130
*/
130131
@AliasFor(annotation = Collation.class, attribute = "value")
131132
String collation() default "";
133+
134+
/**
135+
* The name of the index to use. <br />
136+
* {@code @Query(value = "...", hint = "lastname-idx")} can be used as shortcut for:
137+
*
138+
* <pre class="code">
139+
* &#64;Query(...)
140+
* &#64;Hint("lastname-idx")
141+
* List&lt;User&gt; findAllByLastname(String collation);
142+
* </pre>
143+
*
144+
* @return the index name.
145+
* @since 4.1
146+
* @see Hint#indexName()
147+
*/
148+
@AliasFor(annotation = Hint.class, attribute = "indexName")
149+
String hint() default "";
132150
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, C
135135
applyQueryMetaAttributesWhenPresent(query);
136136
query = applyAnnotatedDefaultSortIfPresent(query);
137137
query = applyAnnotatedCollationIfPresent(query, accessor);
138+
query = applyHintIfPresent(query);
138139

139140
FindWithQuery<?> find = typeToRead == null //
140141
? executableFind //
@@ -225,6 +226,21 @@ Query applyAnnotatedCollationIfPresent(Query query, ConvertingParameterAccessor
225226
accessor, getQueryMethod().getParameters(), expressionParser, evaluationContextProvider);
226227
}
227228

229+
/**
230+
* If present apply the hint from the {@link org.springframework.data.mongodb.repository.Hint} annotation.
231+
*
232+
* @param query must not be {@literal null}.
233+
* @return never {@literal null}.
234+
* @since 4.1
235+
*/
236+
Query applyHintIfPresent(Query query) {
237+
238+
if(!method.hasAnnotatedHint()) {
239+
return query;
240+
}
241+
return query.withHint(method.getAnnotatedHint());
242+
}
243+
228244
/**
229245
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
230246
* {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ protected Publisher<Object> doExecute(ReactiveMongoQueryMethod method, ResultPro
160160
applyQueryMetaAttributesWhenPresent(query);
161161
query = applyAnnotatedDefaultSortIfPresent(query);
162162
query = applyAnnotatedCollationIfPresent(query, accessor);
163+
query = applyHintIfPresent(query);
163164

164165
FindWithQuery<?> find = typeToRead == null //
165166
? findOperationWithProjection //
@@ -269,6 +270,21 @@ Query applyAnnotatedCollationIfPresent(Query query, ConvertingParameterAccessor
269270
accessor, getQueryMethod().getParameters(), expressionParser, evaluationContextProvider);
270271
}
271272

273+
/**
274+
* If present apply the hint from the {@link org.springframework.data.mongodb.repository.Hint} annotation.
275+
*
276+
* @param query must not be {@literal null}.
277+
* @return never {@literal null}.
278+
* @since 4.1
279+
*/
280+
Query applyHintIfPresent(Query query) {
281+
282+
if(!method.hasAnnotatedHint()) {
283+
return query;
284+
}
285+
return query.withHint(method.getAnnotatedHint());
286+
}
287+
272288
/**
273289
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
274290
* {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,21 @@ static AggregationOptions.Builder applyMeta(AggregationOptions.Builder builder,
102102
return builder;
103103
}
104104

105+
/**
106+
* If present apply the hint from the {@link org.springframework.data.mongodb.repository.Hint} annotation.
107+
*
108+
* @param builder must not be {@literal null}.
109+
* @return never {@literal null}.
110+
* @since 4.1
111+
*/
112+
static AggregationOptions.Builder applyHint(AggregationOptions.Builder builder, MongoQueryMethod queryMethod) {
113+
114+
if(!queryMethod.hasAnnotatedHint()) {
115+
return builder;
116+
}
117+
return builder.hint(queryMethod.getAnnotatedHint());
118+
}
119+
105120
/**
106121
* Append {@code $sort} aggregation stage if {@link ConvertingParameterAccessor#getSort()} is present.
107122
*

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
3434
import org.springframework.data.mongodb.core.query.UpdateDefinition;
3535
import org.springframework.data.mongodb.repository.Aggregation;
36+
import org.springframework.data.mongodb.repository.Hint;
3637
import org.springframework.data.mongodb.repository.Meta;
3738
import org.springframework.data.mongodb.repository.Query;
3839
import org.springframework.data.mongodb.repository.Tailable;
@@ -362,6 +363,27 @@ public String[] getAnnotatedAggregation() {
362363
"Expected to find @Aggregation annotation but did not; Make sure to check hasAnnotatedAggregation() before."));
363364
}
364365

366+
/**
367+
* @return {@literal true} if the {@link Hint} annotation is present and the index name is not empty.
368+
* @since 4.1
369+
*/
370+
public boolean hasAnnotatedHint() {
371+
return StringUtils.hasText(getAnnotatedHint());
372+
}
373+
374+
/**
375+
* Returns the aggregation pipeline declared via a {@link Hint} annotation.
376+
*
377+
* @return the index name (might be empty) or {@literal null} if not present.
378+
* @since 4.1
379+
*/
380+
@Nullable
381+
public String getAnnotatedHint() {
382+
383+
Optional<Hint> hint = doFindAnnotation(Hint.class);
384+
return hint.map(Hint::indexName).orElse(null);
385+
}
386+
365387
private Optional<String[]> findAnnotatedAggregation() {
366388

367389
return lookupAggregationAnnotation() //

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingPar
128128
AggregationUtils.applyCollation(builder, method.getAnnotatedCollation(), accessor, method.getParameters(),
129129
expressionParser, evaluationContextProvider);
130130
AggregationUtils.applyMeta(builder, method);
131+
AggregationUtils.applyHint(builder, method);
131132

132133
return builder.build();
133134
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.data.mongodb.core.aggregation.Aggregation;
3030
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
3131
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
32+
import org.springframework.data.mongodb.core.aggregation.AggregationOptions.Builder;
3233
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
3334
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
3435
import org.springframework.data.mongodb.core.convert.MongoConverter;
@@ -178,6 +179,7 @@ private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingPar
178179
AggregationUtils.applyCollation(builder, method.getAnnotatedCollation(), accessor, method.getParameters(),
179180
expressionParser, evaluationContextProvider);
180181
AggregationUtils.applyMeta(builder, method);
182+
AggregationUtils.applyHint(builder, method);
181183

182184
return builder.build();
183185
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.springframework.data.mongodb.core.query.Collation;
6262
import org.springframework.data.mongodb.core.query.Query;
6363
import org.springframework.data.mongodb.core.query.UpdateDefinition;
64+
import org.springframework.data.mongodb.repository.Hint;
6465
import org.springframework.data.mongodb.repository.Meta;
6566
import org.springframework.data.mongodb.repository.MongoRepository;
6667
import org.springframework.data.mongodb.repository.Update;
@@ -458,7 +459,7 @@ void collationParameterShouldNotBeAppliedWhenNullOverrideAnnotation() {
458459
void updateExecutionCallsUpdateAllCorrectly() {
459460

460461
when(terminatingUpdate.all()).thenReturn(updateResultMock);
461-
462+
462463
createQueryForMethod("findAndIncreaseVisitsByLastname", String.class, int.class) //
463464
.execute(new Object[] { "dalinar", 100 });
464465

@@ -469,6 +470,29 @@ void updateExecutionCallsUpdateAllCorrectly() {
469470
assertThat(update.getValue().getUpdateObject()).isEqualTo(Document.parse("{ '$inc' : { 'visits' : 100 } }"));
470471
}
471472

473+
@Test // GH-3230
474+
void findShouldApplyHint() {
475+
476+
createQueryForMethod("findWithHintByFirstname", String.class).execute(new Object[] { "Jasna" });
477+
478+
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
479+
verify(withQueryMock).matching(captor.capture());
480+
assertThat(captor.getValue().getHint()).isEqualTo("idx-fn");
481+
}
482+
483+
@Test // GH-3230
484+
void updateShouldApplyHint() {
485+
486+
when(terminatingUpdate.all()).thenReturn(updateResultMock);
487+
488+
createQueryForMethod("findAndIncreaseVisitsByLastname", String.class, int.class) //
489+
.execute(new Object[] { "dalinar", 100 });
490+
491+
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
492+
verify(executableUpdate).matching(captor.capture());
493+
assertThat(captor.getValue().getHint()).isEqualTo("idx-ln");
494+
}
495+
472496
private MongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
473497
return createQueryForMethod(Repo.class, methodName, paramTypes);
474498
}
@@ -584,8 +608,12 @@ private interface Repo extends MongoRepository<Person, Long> {
584608
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
585609
List<Person> findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation);
586610

611+
@Hint("idx-ln")
587612
@Update("{ '$inc' : { 'visits' : ?1 } }")
588613
void findAndIncreaseVisitsByLastname(String lastname, int value);
614+
615+
@Hint("idx-fn")
616+
void findWithHintByFirstname(String firstname);
589617
}
590618

591619
// DATAMONGO-1872

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818
import static org.assertj.core.api.Assertions.*;
1919
import static org.mockito.Mockito.*;
2020

21+
import com.mongodb.MongoClientSettings;
22+
import com.mongodb.client.result.UpdateResult;
23+
import org.bson.codecs.configuration.CodecRegistry;
24+
import org.springframework.data.mongodb.core.ReactiveUpdateOperation.TerminatingUpdate;
25+
import org.springframework.data.mongodb.core.ReactiveUpdateOperation.ReactiveUpdate;
26+
import org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithQuery;
27+
import org.springframework.data.mongodb.core.query.UpdateDefinition;
28+
import org.springframework.data.mongodb.repository.Hint;
29+
import org.springframework.data.mongodb.repository.Update;
2130
import reactor.core.publisher.Flux;
2231
import reactor.core.publisher.Mono;
2332

@@ -71,6 +80,9 @@ class AbstractReactiveMongoQueryUnitTests {
7180

7281
@Mock ReactiveFind<?> executableFind;
7382
@Mock FindWithQuery<?> withQueryMock;
83+
@Mock ReactiveUpdate executableUpdate;
84+
@Mock UpdateWithQuery updateWithQuery;
85+
@Mock TerminatingUpdate terminatingUpdate;
7486

7587
@BeforeEach
7688
void setUp() {
@@ -91,6 +103,11 @@ void setUp() {
91103
doReturn(Flux.empty()).when(withQueryMock).all();
92104
doReturn(Mono.empty()).when(withQueryMock).first();
93105
doReturn(Mono.empty()).when(withQueryMock).one();
106+
107+
doReturn(executableUpdate).when(mongoOperationsMock).update(any());
108+
doReturn(executableUpdate).when(executableUpdate).inCollection(anyString());
109+
doReturn(updateWithQuery).when(executableUpdate).matching(any(Query.class));
110+
doReturn(terminatingUpdate).when(updateWithQuery).apply(any(UpdateDefinition.class));
94111
}
95112

96113
@Test // DATAMONGO-1854
@@ -223,6 +240,29 @@ void collationParameterShouldNotBeAppliedWhenNullOverrideAnnotation() {
223240
.contains(Collation.of("en_US").toDocument());
224241
}
225242

243+
@Test // GH-3230
244+
void findShouldApplyHint() {
245+
246+
createQueryForMethod("findWithHintByFirstname", String.class).executeBlocking(new Object[] { "Jasna" });
247+
248+
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
249+
verify(withQueryMock).matching(captor.capture());
250+
assertThat(captor.getValue().getHint()).isEqualTo("idx-fn");
251+
}
252+
253+
@Test // GH-3230
254+
void updateShouldApplyHint() {
255+
256+
when(terminatingUpdate.all()).thenReturn(Mono.just(mock(UpdateResult.class)));
257+
258+
createQueryForMethod("findAndIncreaseVisitsByLastname", String.class, int.class) //
259+
.executeBlocking(new Object[] { "dalinar", 100 });
260+
261+
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
262+
verify(executableUpdate).matching(captor.capture());
263+
assertThat(captor.getValue().getHint()).isEqualTo("idx-ln");
264+
}
265+
226266
private ReactiveMongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
227267
return createQueryForMethod(Repo.class, methodName, paramTypes);
228268
}
@@ -291,6 +331,11 @@ public ReactiveMongoQueryFake setLimitingQuery(boolean limitingQuery) {
291331
isLimitingQuery = limitingQuery;
292332
return this;
293333
}
334+
335+
@Override
336+
protected Mono<CodecRegistry> getCodecRegistry() {
337+
return Mono.just(MongoClientSettings.getDefaultCodecRegistry());
338+
}
294339
}
295340

296341
private interface Repo extends ReactiveMongoRepository<Person, Long> {
@@ -315,5 +360,12 @@ List<Person> findWithCollationUsingPlaceholdersInDocumentByFirstName(String firs
315360

316361
@org.springframework.data.mongodb.repository.Query(collation = "{ 'locale' : 'en_US' }")
317362
List<Person> findWithWithCollationParameterAndAnnotationByFirstName(String firstname, Collation collation);
363+
364+
@Hint("idx-ln")
365+
@Update("{ '$inc' : { 'visits' : ?1 } }")
366+
void findAndIncreaseVisitsByLastname(String lastname, int value);
367+
368+
@Hint("idx-fn")
369+
void findWithHintByFirstname(String firstname);
318370
}
319371
}

0 commit comments

Comments
 (0)