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 18f8b269be..a3928e40ae 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 @@ -33,6 +33,7 @@ * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch + * @author Jorge Rodríguez */ @Collation @Retention(RetentionPolicy.RUNTIME) @@ -40,6 +41,7 @@ @Documented @QueryAnnotation @Hint +@ReadPreference public @interface Query { /** @@ -147,4 +149,21 @@ */ @AliasFor(annotation = Hint.class, attribute = "indexName") String hint() default ""; + + /** + * The mode of the read preference to use.
+ * {@code @Query(value = "...", readPreference = "secondary")} can be used as shortcut for: + * + *
+	 * @Query(...)
+	 * @ReadPreference("secondary")
+	 * List<User> findAllByLastname(String collation);
+	 * 
+ * + * @return the index name. + * @since 4.2 + * @see ReadPreference#value() + */ + @AliasFor(annotation = ReadPreference.class, attribute = "value") + String readPreference() default ""; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ReadPreference.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ReadPreference.java new file mode 100644 index 0000000000..2ba4e32fd7 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ReadPreference.java @@ -0,0 +1,53 @@ +/* + * Copyright 2011-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; + +/** + * Annotation to declare read preference for repository and query. + * + * @author Jorge Rodríguez + * @since 4.2 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +public @interface ReadPreference { + + /** + * Configure read preference mode + * @return read preference mode + */ + String value() default ""; + + /** + * Set read preference tags + * @return read preference tags + */ + ReadPreferenceTag[] tags() default {}; + + /** + * Set read preference maxStalenessSeconds + * @return read preference maxStalenessSeconds + */ + long maxStalenessSeconds() default -1; +} + diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ReadPreferenceTag.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ReadPreferenceTag.java new file mode 100644 index 0000000000..5fae877ee2 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ReadPreferenceTag.java @@ -0,0 +1,37 @@ +/* + * Copyright 2011-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; + +/** + * Annotation used by {@link ReadPreference} for define {@link com.mongodb.Tag} + * + * @author Jorge Rodríguez + * @since 4.2 + */ +public @interface ReadPreferenceTag { + + /** + * Set the name of tag + * @return name of tag + */ + String name(); + + /** + * Set the value of tag + * @return value of tag + */ + String value(); +} 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 e8d69504d7..0913a9fb4b 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 @@ -62,6 +62,7 @@ * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch + * @author Jorge Rodríguez */ public abstract class AbstractMongoQuery implements RepositoryQuery { @@ -137,6 +138,7 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, C query = applyAnnotatedDefaultSortIfPresent(query); query = applyAnnotatedCollationIfPresent(query, accessor); query = applyHintIfPresent(query); + query = applyAnnotatedReadPreferenceIfPresent(query); FindWithQuery find = typeToRead == null // ? executableFind // @@ -145,6 +147,22 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, C return getExecution(accessor, find).execute(query); } + /** + * If present apply the {@link com.mongodb.ReadPreference} from the {@link org.springframework.data.mongodb.repository.ReadPreference} annotation. + * + * @param query must not be {@literal null}. + * @return never {@literal null}. + * @since 4.2 + */ + private Query applyAnnotatedReadPreferenceIfPresent(Query query) { + + if (!method.hasAnnotatedReadPreference()) { + return query; + } + + return query.withReadPreference(method.getAnnotatedReadPreference()); + } + private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery operation) { if (isDeleteQuery()) { 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 fb430a1517..51301697e5 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 @@ -66,6 +66,7 @@ * * @author Mark Paluch * @author Christoph Strobl + * @author Jorge Rodríguez * @since 2.0 */ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { @@ -161,6 +162,8 @@ protected Publisher doExecute(ReactiveMongoQueryMethod method, ResultPro query = applyAnnotatedDefaultSortIfPresent(query); query = applyAnnotatedCollationIfPresent(query, accessor); query = applyHintIfPresent(query); + query = applyAnnotatedReadPreferenceIfPresent(query); + FindWithQuery find = typeToRead == null // ? findOperationWithProjection // @@ -229,6 +232,7 @@ private boolean isTailable(MongoQueryMethod method) { return method.getTailableAnnotation() != null; } + Query applyQueryMetaAttributesWhenPresent(Query query) { if (method.hasQueryMetaAttributes()) { @@ -286,6 +290,22 @@ Query applyHintIfPresent(Query query) { return query.withHint(method.getAnnotatedHint()); } + /** + * If present apply the {@link com.mongodb.ReadPreference} from the {@link org.springframework.data.mongodb.repository.ReadPreference} annotation. + * + * @param query must not be {@literal null}. + * @return never {@literal null}. + * @since 4.2 + */ + private Query applyAnnotatedReadPreferenceIfPresent(Query query) { + + if (!method.hasAnnotatedReadPreference()) { + return query; + } + + return query.withReadPreference(method.getAnnotatedReadPreference()); + } + /** * 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/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java index 2f8ac76399..f4f30a5a93 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 @@ -22,7 +22,11 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import com.mongodb.Tag; +import com.mongodb.TagSet; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.geo.GeoPage; import org.springframework.data.geo.GeoResult; @@ -36,6 +40,7 @@ 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.ReadPreference; import org.springframework.data.mongodb.repository.Tailable; import org.springframework.data.mongodb.repository.Update; import org.springframework.data.projection.ProjectionFactory; @@ -57,6 +62,7 @@ * @author Oliver Gierke * @author Christoph Strobl * @author Mark Paluch + * @author Jorge Rodríguez */ public class MongoQueryMethod extends QueryMethod { @@ -314,6 +320,58 @@ public String getAnnotatedSort() { "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} or {@link Aggregation} having a non-empty collation attribute. + * @since 4.2 + */ + public boolean hasAnnotatedReadPreference() { + return doFindReadPreferenceAnnotation().map(ReadPreference::value).filter(StringUtils::hasText).isPresent(); + } + + /** + * Get the {@link com.mongodb.ReadPreference} extracted from the {@link ReadPreference} annotation. + * + * @return the {@link ReadPreference()}. + * @throws IllegalStateException if method not annotated with {@link Query}. Make sure to check + * {@link #hasAnnotatedQuery()} first. + * @since 4.2 + */ + public com.mongodb.ReadPreference getAnnotatedReadPreference() { + + return doFindReadPreferenceAnnotation().map(annotationReadPreference -> { + + com.mongodb.ReadPreference readPreference = com.mongodb.ReadPreference.valueOf(annotationReadPreference.value()); + + if (annotationReadPreference.tags().length > 0) { + List tags = Arrays.stream(annotationReadPreference.tags()) + .map(tag -> new Tag(tag.name(), tag.value())) + .collect(Collectors.toList()); + readPreference = readPreference.withTagSet(new TagSet(tags)); + } + + if (annotationReadPreference.maxStalenessSeconds() > 0) { + readPreference = readPreference.withMaxStalenessMS(annotationReadPreference.maxStalenessSeconds(), TimeUnit.SECONDS); + } + + return readPreference; + }).orElseThrow(() -> new IllegalStateException( + "Expected to find @ReadPreference annotation but did not; Make sure to check hasAnnotatedReadPreference() before.")); + } + + /** + * Get {@link com.mongodb.ReadPreference} from query. First check if the method is annotated. If not, check if the class is annotated. + * So if the method and the class are annotated with @ReadPreference, the method annotation takes precedence. + * @return the {@link com.mongodb.ReadPreference} + * @since 4.2 + */ + private Optional doFindReadPreferenceAnnotation() { + return doFindAnnotation(ReadPreference.class).or(() -> doFindAnnotationInClass(ReadPreference.class)); + } + + /** * Check if the query method is decorated with an non empty {@link Query#collation()} or or * {@link Aggregation#collation()}. @@ -400,11 +458,21 @@ Optional lookupUpdateAnnotation() { @SuppressWarnings("unchecked") private Optional doFindAnnotation(Class annotationType) { + return (Optional) this.annotationCache.computeIfAbsent(annotationType, it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it))); } + @SuppressWarnings("unchecked") + private Optional doFindAnnotationInClass(Class annotationType) { + + Optional mergedAnnotation = Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), annotationType)); + annotationCache.put(annotationType, mergedAnnotation); + + return (Optional) mergedAnnotation; + } + @Override public boolean isModifyingQuery() { return isModifying.get(); 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 0fc4d8b73f..7c416f5caa 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 @@ -23,7 +23,11 @@ import java.util.List; import java.util.Locale; import java.util.Optional; +import java.util.concurrent.TimeUnit; +import com.mongodb.Tag; +import com.mongodb.TagSet; +import com.mongodb.TaggableReadPreference; import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; import org.bson.types.ObjectId; @@ -65,6 +69,8 @@ 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.ReadPreference; +import org.springframework.data.mongodb.repository.ReadPreferenceTag; import org.springframework.data.mongodb.repository.Update; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -84,6 +90,7 @@ * @author Oliver Gierke * @author Thomas Darimont * @author Mark Paluch + * @author Jorge Rodríguez */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -518,6 +525,33 @@ void sortAndLimitShouldBeAppliedToQuery() { assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("fn", 1)); } + @Test // GH-2971 + void findShouldApplyReadPreference() { + + createQueryForMethod("findWithReadPreferenceByFirstname", String.class).execute(new Object[] { "Jasna" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getReadPreference().getName()).isEqualTo("secondaryPreferred"); + assertThat(((TaggableReadPreference)captor.getValue().getReadPreference()).getTagSetList()) + .containsExactly(new TagSet(List.of(new Tag("local", "east"), new Tag("pre", "west")))); + assertThat(((TaggableReadPreference)captor.getValue().getReadPreference()).getMaxStaleness(TimeUnit.SECONDS)).isEqualTo(99); + } + + @Test // GH-2971 + void findShouldApplyReadPreferenceAtRepository() { + + createQueryForMethod("findWithLimit", String.class, Limit.class).execute(new Object[] { "dalinar", Limit.of(42) }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getReadPreference().getName()).isEqualTo("primaryPreferred"); + assertThat(((TaggableReadPreference)captor.getValue().getReadPreference()).getTagSetList()) + .containsExactly(new TagSet(List.of(new Tag("primary-local", "east"), new Tag("primary-pre", "west")))); + assertThat(((TaggableReadPreference)captor.getValue().getReadPreference()).getMaxStaleness(TimeUnit.SECONDS)).isEqualTo(20); + } + + private MongoQueryFake createQueryForMethod(String methodName, Class... paramTypes) { return createQueryForMethod(Repo.class, methodName, paramTypes); } @@ -588,6 +622,11 @@ protected CodecRegistry getCodecRegistry() { } } + @ReadPreference( + value = "primaryPreferred", + tags = { @ReadPreferenceTag(name = "primary-local", value = "east"), @ReadPreferenceTag(name = "primary-pre", value = "west") }, + maxStalenessSeconds = 20 + ) private interface Repo extends MongoRepository { List deleteByLastname(String lastname); @@ -643,6 +682,13 @@ private interface Repo extends MongoRepository { List findWithLimit(String firstname, Limit limit); List findWithSortAndLimit(String firstname, Sort sort, Limit limit); + + @ReadPreference( + value = "secondaryPreferred", + tags = { @ReadPreferenceTag(name = "local", value = "east"), @ReadPreferenceTag(name = "pre", value = "west") }, + maxStalenessSeconds = 99 + ) + List findWithReadPreferenceByFirstname(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 e7fc854006..df882b642e 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 @@ -19,6 +19,9 @@ import static org.mockito.Mockito.*; import com.mongodb.MongoClientSettings; +import com.mongodb.Tag; +import com.mongodb.TagSet; +import com.mongodb.TaggableReadPreference; import com.mongodb.client.result.UpdateResult; import org.bson.codecs.configuration.CodecRegistry; import org.springframework.data.mongodb.core.ReactiveUpdateOperation.TerminatingUpdate; @@ -26,6 +29,8 @@ 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.ReadPreference; +import org.springframework.data.mongodb.repository.ReadPreferenceTag; import org.springframework.data.mongodb.repository.Update; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -33,6 +38,7 @@ import java.lang.reflect.Method; import java.util.List; import java.util.Locale; +import java.util.concurrent.TimeUnit; import org.bson.Document; import org.junit.jupiter.api.BeforeEach; @@ -68,7 +74,7 @@ * * @author Christoph Strobl * @author Mark Paluch - * @currentRead Way of Kings - Brandon Sanderson + * @author Jorge Rodríguez */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -263,6 +269,19 @@ void updateShouldApplyHint() { assertThat(captor.getValue().getHint()).isEqualTo("idx-ln"); } + @Test // GH-2971 + void findShouldApplyReadPreference() { + + createQueryForMethod("findWithReadPreferenceByFirstname", String.class).executeBlocking(new Object[] { "Jasna" }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Query.class); + verify(withQueryMock).matching(captor.capture()); + assertThat(captor.getValue().getReadPreference().getName()).isEqualTo("secondaryPreferred"); + assertThat(((TaggableReadPreference)captor.getValue().getReadPreference()).getTagSetList()) + .containsExactly(new TagSet(List.of(new Tag("local", "east"), new Tag("pre", "west")))); + assertThat(((TaggableReadPreference)captor.getValue().getReadPreference()).getMaxStaleness(TimeUnit.SECONDS)).isEqualTo(99); + } + private ReactiveMongoQueryFake createQueryForMethod(String methodName, Class... paramTypes) { return createQueryForMethod(Repo.class, methodName, paramTypes); } @@ -367,5 +386,12 @@ List findWithCollationUsingPlaceholdersInDocumentByFirstName(String firs @Hint("idx-fn") void findWithHintByFirstname(String firstname); + + @ReadPreference( + value = "secondaryPreferred", + tags = { @ReadPreferenceTag(name = "local", value = "east"), @ReadPreferenceTag(name = "pre", value = "west") }, + maxStalenessSeconds = 99 + ) + Flux findWithReadPreferenceByFirstname(String firstname); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java index c0517eafb1..53ae7a404a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java @@ -41,6 +41,7 @@ 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.mongodb.repository.ReadPreference; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; @@ -52,6 +53,7 @@ * @author Oliver Gierke * @author Christoph Strobl * @author Mark Paluch + * @author Jorge Rodríguez */ public class MongoQueryMethodUnitTests { @@ -311,6 +313,43 @@ void annotatedCollationClashSelectsAtCollationAnnotationValue() throws Exception assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT"); } + @Test // GH-2971 + void readsReadPreferenceAtQueryAnnotation() throws Exception { + + MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithReadPreferenceFromAtReadPreferenceByFirstname", String.class); + + assertThat(method.hasAnnotatedReadPreference()).isTrue(); + assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred"); + } + + @Test // GH-2971 + void readsReadPreferenceFromAtQueryAnnotation() throws Exception { + + MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithReadPreferenceFromAtQueryByFirstname", String.class); + + assertThat(method.hasAnnotatedReadPreference()).isTrue(); + assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred"); + } + + @Test // GH-2971 + void annotatedReadPreferenceClashSelectsAtReadPreferenceAnnotationValue() throws Exception { + + MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithMultipleReadPreferencesFromAtQueryAndAtReadPreferenceByFirstname", String.class); + + assertThat(method.hasAnnotatedReadPreference()).isTrue(); + assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred"); + + } + + @Test // GH-2971 + void readsReadPreferenceAtRepositoryAnnotation() throws Exception { + + MongoQueryMethod method = queryMethod(PersonRepository.class, "deleteByUserName", String.class); + + assertThat(method.hasAnnotatedReadPreference()).isTrue(); + assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("primaryPreferred"); + } + private MongoQueryMethod queryMethod(Class repository, String name, Class... parameters) throws Exception { Method method = repository.getMethod(name, parameters); @@ -318,6 +357,7 @@ private MongoQueryMethod queryMethod(Class repository, String name, Class. return new MongoQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context); } + @ReadPreference(value = "primaryPreferred") interface PersonRepository extends Repository { // Misses Pageable @@ -381,6 +421,16 @@ interface PersonRepository extends Repository { @Collation("de_AT") @Query(collation = "en_US") List findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname); + + @ReadPreference("secondaryPreferred") + List findWithReadPreferenceFromAtReadPreferenceByFirstname(String firstname); + + @Query(readPreference = "secondaryPreferred") + List findWithReadPreferenceFromAtQueryByFirstname(String firstname); + + @ReadPreference("secondaryPreferred") + @Query(readPreference = "primaryPreferred") + List findWithMultipleReadPreferencesFromAtQueryAndAtReadPreferenceByFirstname(String firstname); } interface SampleRepository extends Repository { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java index 6545b68219..6122da17bd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.annotation.Collation; import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.ReadPreference; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -53,6 +54,7 @@ * * @author Mark Paluch * @author Christoph Strobl + * @author Jorge Rodríguez */ public class ReactiveMongoQueryMethodUnitTests { @@ -226,6 +228,43 @@ void annotatedCollationClashSelectsAtCollationAnnotationValue() throws Exception assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT"); } + + @Test // GH-2971 + void readsReadPreferenceAtQueryAnnotation() throws Exception { + + MongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithReadPreferenceFromAtReadPreferenceByFirstname", String.class); + + assertThat(method.hasAnnotatedReadPreference()).isTrue(); + assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred"); + } + + @Test // GH-2971 + void readsReadPreferenceFromAtQueryAnnotation() throws Exception { + + MongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithReadPreferenceFromAtQueryByFirstname", String.class); + + assertThat(method.hasAnnotatedReadPreference()).isTrue(); + assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred"); + } + + @Test // GH-2971 + void annotatedReadPreferenceClashSelectsAtReadPreferenceAnnotationValue() throws Exception { + + MongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithMultipleReadPreferencesFromAtQueryAndAtReadPreferenceByFirstname", String.class); + + assertThat(method.hasAnnotatedReadPreference()).isTrue(); + assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred"); + } + + @Test // GH-2971 + void readsReadPreferenceAtRepositoryAnnotation() throws Exception { + + MongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "deleteByUserName", String.class); + + assertThat(method.hasAnnotatedReadPreference()).isTrue(); + assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("primaryPreferred"); + } + private ReactiveMongoQueryMethod queryMethod(Class repository, String name, Class... parameters) throws Exception { @@ -234,6 +273,7 @@ private ReactiveMongoQueryMethod queryMethod(Class repository, String name, C return new ReactiveMongoQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context); } + @ReadPreference(value = "primaryPreferred") interface PersonRepository extends Repository { Mono findMonoByLastname(String lastname, Pageable pageRequest); @@ -277,6 +317,16 @@ interface PersonRepository extends Repository { @Collation("de_AT") @Query(collation = "en_US") List findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname); + + @ReadPreference("secondaryPreferred") + Flux findWithReadPreferenceFromAtReadPreferenceByFirstname(String firstname); + + @Query(readPreference = "secondaryPreferred") + Flux findWithReadPreferenceFromAtQueryByFirstname(String firstname); + + @ReadPreference("secondaryPreferred") + @Query(readPreference = "primaryPreferred") + Flux findWithMultipleReadPreferencesFromAtQueryAndAtReadPreferenceByFirstname(String firstname); } interface SampleRepository extends Repository { diff --git a/src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc b/src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc index 0b3cef574c..129da7b752 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc @@ -824,3 +824,33 @@ interface GameRepository extends Repository { <2> Instead of `@Query(collation=...)`. <3> Favors `@Collation` over meta usage. ==== + +== Read Preferences + +The `@ReadPreference` annotation allows you to configure MongoDB's ReadPreferences + +.Example of read preferences +==== +[source,java] +---- + +@ReadPreference( + value = "primaryPreferred", + tags = {@ReadPreferenceTag(name = "local", value = "east"), @ReadPreferenceTag(name = "pre", value = "west")}, + maxStalenessSeconds = 150 +) <1> +public interface PersonRepository extends CrudRepository { + + @ReadPreference(value = "secondaryPreferred") <2> + List findWithReadPreferenceAnnotationByLastname(String lastname); + + @Query(readPreference = "nearest") <3> + List findWithReadPreferenceAtTagByFirstname(String firstname); + + List findWithReadPreferenceAtTagByFirstname(String firstname); <4> +---- +<1> Configure read preference for all repository operations that do not have a query-level definition. Therefore, in this case the read preference mode will be `primaryPreferred` +<2> Use the read preference mode defined in annotation `ReadPreference`, in this case secondaryPreferred +<3> The `@Query` annotation defines the `read preference mode` alias which is equivalent to adding the `@ReadPreference` annotation. +<4> This query will use the read preference mode defined in the repository. +====