Skip to content

Commit f2ce489

Browse files
jorgerodchristophstrobl
authored andcommitted
Add annotation ReadPreference.
Closes: #2971
1 parent eb33a6a commit f2ce489

File tree

11 files changed

+418
-1
lines changed

11 files changed

+418
-1
lines changed

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

+19
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@
3333
* @author Thomas Darimont
3434
* @author Christoph Strobl
3535
* @author Mark Paluch
36+
* @author Jorge Rodríguez
3637
*/
3738
@Collation
3839
@Retention(RetentionPolicy.RUNTIME)
3940
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
4041
@Documented
4142
@QueryAnnotation
4243
@Hint
44+
@ReadPreference
4345
public @interface Query {
4446

4547
/**
@@ -147,4 +149,21 @@
147149
*/
148150
@AliasFor(annotation = Hint.class, attribute = "indexName")
149151
String hint() default "";
152+
153+
/**
154+
* The mode of the read preference to use. <br />
155+
* {@code @Query(value = "...", readPreference = "secondary")} can be used as shortcut for:
156+
*
157+
* <pre class="code">
158+
* &#64;Query(...)
159+
* &#64;ReadPreference("secondary")
160+
* List&lt;User&gt; findAllByLastname(String collation);
161+
* </pre>
162+
*
163+
* @return the index name.
164+
* @since 4.2
165+
* @see ReadPreference#value()
166+
*/
167+
@AliasFor(annotation = ReadPreference.class, attribute = "value")
168+
String readPreference() default "";
150169
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2011-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+
/**
25+
* Annotation to declare read preference for repository and query.
26+
*
27+
* @author Jorge Rodríguez
28+
* @since 4.2
29+
*/
30+
@Retention(RetentionPolicy.RUNTIME)
31+
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
32+
@Documented
33+
public @interface ReadPreference {
34+
35+
/**
36+
* Configure read preference mode
37+
* @return read preference mode
38+
*/
39+
String value() default "";
40+
41+
/**
42+
* Set read preference tags
43+
* @return read preference tags
44+
*/
45+
ReadPreferenceTag[] tags() default {};
46+
47+
/**
48+
* Set read preference maxStalenessSeconds
49+
* @return read preference maxStalenessSeconds
50+
*/
51+
long maxStalenessSeconds() default -1;
52+
}
53+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2011-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+
/**
19+
* Annotation used by {@link ReadPreference} for define {@link com.mongodb.Tag}
20+
*
21+
* @author Jorge Rodríguez
22+
* @since 4.2
23+
*/
24+
public @interface ReadPreferenceTag {
25+
26+
/**
27+
* Set the name of tag
28+
* @return name of tag
29+
*/
30+
String name();
31+
32+
/**
33+
* Set the value of tag
34+
* @return value of tag
35+
*/
36+
String value();
37+
}

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

+18
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
* @author Thomas Darimont
6363
* @author Christoph Strobl
6464
* @author Mark Paluch
65+
* @author Jorge Rodríguez
6566
*/
6667
public abstract class AbstractMongoQuery implements RepositoryQuery {
6768

@@ -137,6 +138,7 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, C
137138
query = applyAnnotatedDefaultSortIfPresent(query);
138139
query = applyAnnotatedCollationIfPresent(query, accessor);
139140
query = applyHintIfPresent(query);
141+
query = applyAnnotatedReadPreferenceIfPresent(query);
140142

141143
FindWithQuery<?> find = typeToRead == null //
142144
? executableFind //
@@ -145,6 +147,22 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, C
145147
return getExecution(accessor, find).execute(query);
146148
}
147149

150+
/**
151+
* If present apply the {@link com.mongodb.ReadPreference} from the {@link org.springframework.data.mongodb.repository.ReadPreference} annotation.
152+
*
153+
* @param query must not be {@literal null}.
154+
* @return never {@literal null}.
155+
* @since 4.2
156+
*/
157+
private Query applyAnnotatedReadPreferenceIfPresent(Query query) {
158+
159+
if (!method.hasAnnotatedReadPreference()) {
160+
return query;
161+
}
162+
163+
return query.withReadPreference(method.getAnnotatedReadPreference());
164+
}
165+
148166
private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery<?> operation) {
149167

150168
if (isDeleteQuery()) {

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

+20
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
*
6767
* @author Mark Paluch
6868
* @author Christoph Strobl
69+
* @author Jorge Rodríguez
6970
* @since 2.0
7071
*/
7172
public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
@@ -161,6 +162,8 @@ protected Publisher<Object> doExecute(ReactiveMongoQueryMethod method, ResultPro
161162
query = applyAnnotatedDefaultSortIfPresent(query);
162163
query = applyAnnotatedCollationIfPresent(query, accessor);
163164
query = applyHintIfPresent(query);
165+
query = applyAnnotatedReadPreferenceIfPresent(query);
166+
164167

165168
FindWithQuery<?> find = typeToRead == null //
166169
? findOperationWithProjection //
@@ -229,6 +232,7 @@ private boolean isTailable(MongoQueryMethod method) {
229232
return method.getTailableAnnotation() != null;
230233
}
231234

235+
232236
Query applyQueryMetaAttributesWhenPresent(Query query) {
233237

234238
if (method.hasQueryMetaAttributes()) {
@@ -286,6 +290,22 @@ Query applyHintIfPresent(Query query) {
286290
return query.withHint(method.getAnnotatedHint());
287291
}
288292

293+
/**
294+
* If present apply the {@link com.mongodb.ReadPreference} from the {@link org.springframework.data.mongodb.repository.ReadPreference} annotation.
295+
*
296+
* @param query must not be {@literal null}.
297+
* @return never {@literal null}.
298+
* @since 4.2
299+
*/
300+
private Query applyAnnotatedReadPreferenceIfPresent(Query query) {
301+
302+
if (!method.hasAnnotatedReadPreference()) {
303+
return query;
304+
}
305+
306+
return query.withReadPreference(method.getAnnotatedReadPreference());
307+
}
308+
289309
/**
290310
* Creates a {@link Query} instance using the given {@link ConvertingParameterAccessor}. Will delegate to
291311
* {@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/MongoQueryMethod.java

+68
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Optional;
25+
import java.util.concurrent.TimeUnit;
26+
import java.util.stream.Collectors;
2527

28+
import com.mongodb.Tag;
29+
import com.mongodb.TagSet;
2630
import org.springframework.core.annotation.AnnotatedElementUtils;
2731
import org.springframework.data.geo.GeoPage;
2832
import org.springframework.data.geo.GeoResult;
@@ -36,6 +40,7 @@
3640
import org.springframework.data.mongodb.repository.Hint;
3741
import org.springframework.data.mongodb.repository.Meta;
3842
import org.springframework.data.mongodb.repository.Query;
43+
import org.springframework.data.mongodb.repository.ReadPreference;
3944
import org.springframework.data.mongodb.repository.Tailable;
4045
import org.springframework.data.mongodb.repository.Update;
4146
import org.springframework.data.projection.ProjectionFactory;
@@ -57,6 +62,7 @@
5762
* @author Oliver Gierke
5863
* @author Christoph Strobl
5964
* @author Mark Paluch
65+
* @author Jorge Rodríguez
6066
*/
6167
public class MongoQueryMethod extends QueryMethod {
6268

@@ -314,6 +320,58 @@ public String getAnnotatedSort() {
314320
"Expected to find @Query annotation but did not; Make sure to check hasAnnotatedSort() before."));
315321
}
316322

323+
324+
/**
325+
* Check if the query method is decorated with an non empty {@link Query#collation()}.
326+
*
327+
* @return true if method annotated with {@link Query} or {@link Aggregation} having a non-empty collation attribute.
328+
* @since 4.2
329+
*/
330+
public boolean hasAnnotatedReadPreference() {
331+
return doFindReadPreferenceAnnotation().map(ReadPreference::value).filter(StringUtils::hasText).isPresent();
332+
}
333+
334+
/**
335+
* Get the {@link com.mongodb.ReadPreference} extracted from the {@link ReadPreference} annotation.
336+
*
337+
* @return the {@link ReadPreference()}.
338+
* @throws IllegalStateException if method not annotated with {@link Query}. Make sure to check
339+
* {@link #hasAnnotatedQuery()} first.
340+
* @since 4.2
341+
*/
342+
public com.mongodb.ReadPreference getAnnotatedReadPreference() {
343+
344+
return doFindReadPreferenceAnnotation().map(annotationReadPreference -> {
345+
346+
com.mongodb.ReadPreference readPreference = com.mongodb.ReadPreference.valueOf(annotationReadPreference.value());
347+
348+
if (annotationReadPreference.tags().length > 0) {
349+
List<Tag> tags = Arrays.stream(annotationReadPreference.tags())
350+
.map(tag -> new Tag(tag.name(), tag.value()))
351+
.collect(Collectors.toList());
352+
readPreference = readPreference.withTagSet(new TagSet(tags));
353+
}
354+
355+
if (annotationReadPreference.maxStalenessSeconds() > 0) {
356+
readPreference = readPreference.withMaxStalenessMS(annotationReadPreference.maxStalenessSeconds(), TimeUnit.SECONDS);
357+
}
358+
359+
return readPreference;
360+
}).orElseThrow(() -> new IllegalStateException(
361+
"Expected to find @ReadPreference annotation but did not; Make sure to check hasAnnotatedReadPreference() before."));
362+
}
363+
364+
/**
365+
* Get {@link com.mongodb.ReadPreference} from query. First check if the method is annotated. If not, check if the class is annotated.
366+
* So if the method and the class are annotated with @ReadPreference, the method annotation takes precedence.
367+
* @return the {@link com.mongodb.ReadPreference}
368+
* @since 4.2
369+
*/
370+
private Optional<ReadPreference> doFindReadPreferenceAnnotation() {
371+
return doFindAnnotation(ReadPreference.class).or(() -> doFindAnnotationInClass(ReadPreference.class));
372+
}
373+
374+
317375
/**
318376
* Check if the query method is decorated with an non empty {@link Query#collation()} or or
319377
* {@link Aggregation#collation()}.
@@ -400,11 +458,21 @@ Optional<Update> lookupUpdateAnnotation() {
400458

401459
@SuppressWarnings("unchecked")
402460
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
461+
403462

404463
return (Optional<A>) this.annotationCache.computeIfAbsent(annotationType,
405464
it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it)));
406465
}
407466

467+
@SuppressWarnings("unchecked")
468+
private <A extends Annotation> Optional<A> doFindAnnotationInClass(Class<A> annotationType) {
469+
470+
Optional<Annotation> mergedAnnotation = Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), annotationType));
471+
annotationCache.put(annotationType, mergedAnnotation);
472+
473+
return (Optional<A>) mergedAnnotation;
474+
}
475+
408476
@Override
409477
public boolean isModifyingQuery() {
410478
return isModifying.get();

0 commit comments

Comments
 (0)