Browse Source

Add ReadPreference annotation.

The annotation enables a declarative style of defining the ReadPreference to use for individual or all repository queries.

Closes: #2971
Original Pull Request: #4503
pull/4528/head
Jorge Rodríguez Martín 2 years ago committed by Christoph Strobl
parent
commit
16b97a26cc
No known key found for this signature in database
GPG Key ID: 8CC1AB53391458C8
  1. 19
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java
  2. 53
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ReadPreference.java
  3. 37
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ReadPreferenceTag.java
  4. 18
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
  5. 20
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
  6. 68
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
  7. 46
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java
  8. 28
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQueryUnitTests.java
  9. 50
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java
  10. 50
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java
  11. 30
      src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc

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

@ -33,6 +33,7 @@ import org.springframework.data.mongodb.core.annotation.Collation;
* @author Thomas Darimont * @author Thomas Darimont
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
* @author Jorge Rodríguez
*/ */
@Collation @Collation
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ -40,6 +41,7 @@ import org.springframework.data.mongodb.core.annotation.Collation;
@Documented @Documented
@QueryAnnotation @QueryAnnotation
@Hint @Hint
@ReadPreference
public @interface Query { public @interface Query {
/** /**
@ -147,4 +149,21 @@ public @interface Query {
*/ */
@AliasFor(annotation = Hint.class, attribute = "indexName") @AliasFor(annotation = Hint.class, attribute = "indexName")
String hint() default ""; String hint() default "";
/**
* The mode of the read preference to use. <br />
* {@code @Query(value = "...", readPreference = "secondary")} can be used as shortcut for:
*
* <pre class="code">
* &#64;Query(...)
* &#64;ReadPreference("secondary")
* List&lt;User&gt; findAllByLastname(String collation);
* </pre>
*
* @return the index name.
* @since 4.2
* @see ReadPreference#value()
*/
@AliasFor(annotation = ReadPreference.class, attribute = "value")
String readPreference() default "";
} }

53
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;
}

37
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();
}

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

@ -62,6 +62,7 @@ import com.mongodb.client.MongoDatabase;
* @author Thomas Darimont * @author Thomas Darimont
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
* @author Jorge Rodríguez
*/ */
public abstract class AbstractMongoQuery implements RepositoryQuery { public abstract class AbstractMongoQuery implements RepositoryQuery {
@ -137,6 +138,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
query = applyAnnotatedDefaultSortIfPresent(query); query = applyAnnotatedDefaultSortIfPresent(query);
query = applyAnnotatedCollationIfPresent(query, accessor); query = applyAnnotatedCollationIfPresent(query, accessor);
query = applyHintIfPresent(query); query = applyHintIfPresent(query);
query = applyAnnotatedReadPreferenceIfPresent(query);
FindWithQuery<?> find = typeToRead == null // FindWithQuery<?> find = typeToRead == null //
? executableFind // ? executableFind //
@ -145,6 +147,22 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
return getExecution(accessor, find).execute(query); 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) { private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery<?> operation) {
if (isDeleteQuery()) { if (isDeleteQuery()) {

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

@ -66,6 +66,7 @@ import com.mongodb.MongoClientSettings;
* *
* @author Mark Paluch * @author Mark Paluch
* @author Christoph Strobl * @author Christoph Strobl
* @author Jorge Rodríguez
* @since 2.0 * @since 2.0
*/ */
public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
@ -161,6 +162,8 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
query = applyAnnotatedDefaultSortIfPresent(query); query = applyAnnotatedDefaultSortIfPresent(query);
query = applyAnnotatedCollationIfPresent(query, accessor); query = applyAnnotatedCollationIfPresent(query, accessor);
query = applyHintIfPresent(query); query = applyHintIfPresent(query);
query = applyAnnotatedReadPreferenceIfPresent(query);
FindWithQuery<?> find = typeToRead == null // FindWithQuery<?> find = typeToRead == null //
? findOperationWithProjection // ? findOperationWithProjection //
@ -229,6 +232,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
return method.getTailableAnnotation() != null; return method.getTailableAnnotation() != null;
} }
Query applyQueryMetaAttributesWhenPresent(Query query) { Query applyQueryMetaAttributesWhenPresent(Query query) {
if (method.hasQueryMetaAttributes()) { if (method.hasQueryMetaAttributes()) {
@ -286,6 +290,22 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
return query.withHint(method.getAnnotatedHint()); 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 * 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 * {@link #createQuery(ConvertingParameterAccessor)} by default but allows customization of the count query to be

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

@ -22,7 +22,11 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; 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.core.annotation.AnnotatedElementUtils;
import org.springframework.data.geo.GeoPage; import org.springframework.data.geo.GeoPage;
import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResult;
@ -36,6 +40,7 @@ import org.springframework.data.mongodb.repository.Aggregation;
import org.springframework.data.mongodb.repository.Hint; import org.springframework.data.mongodb.repository.Hint;
import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.Meta;
import org.springframework.data.mongodb.repository.Query; 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.Tailable;
import org.springframework.data.mongodb.repository.Update; import org.springframework.data.mongodb.repository.Update;
import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionFactory;
@ -57,6 +62,7 @@ import org.springframework.util.StringUtils;
* @author Oliver Gierke * @author Oliver Gierke
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
* @author Jorge Rodríguez
*/ */
public class MongoQueryMethod extends QueryMethod { public class MongoQueryMethod extends QueryMethod {
@ -314,6 +320,58 @@ public class MongoQueryMethod extends QueryMethod {
"Expected to find @Query annotation but did not; Make sure to check hasAnnotatedSort() before.")); "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<Tag> 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<ReadPreference> 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 * Check if the query method is decorated with an non empty {@link Query#collation()} or or
* {@link Aggregation#collation()}. * {@link Aggregation#collation()}.
@ -401,10 +459,20 @@ public class MongoQueryMethod extends QueryMethod {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) { private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
return (Optional<A>) this.annotationCache.computeIfAbsent(annotationType, return (Optional<A>) this.annotationCache.computeIfAbsent(annotationType,
it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it))); it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it)));
} }
@SuppressWarnings("unchecked")
private <A extends Annotation> Optional<A> doFindAnnotationInClass(Class<A> annotationType) {
Optional<Annotation> mergedAnnotation = Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), annotationType));
annotationCache.put(annotationType, mergedAnnotation);
return (Optional<A>) mergedAnnotation;
}
@Override @Override
public boolean isModifyingQuery() { public boolean isModifyingQuery() {
return isModifying.get(); return isModifying.get();

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

@ -23,7 +23,11 @@ import java.lang.reflect.Method;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; 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.Document;
import org.bson.codecs.configuration.CodecRegistry; import org.bson.codecs.configuration.CodecRegistry;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
@ -65,6 +69,8 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.repository.Hint; import org.springframework.data.mongodb.repository.Hint;
import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.Meta;
import org.springframework.data.mongodb.repository.MongoRepository; 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.mongodb.repository.Update;
import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
@ -84,6 +90,7 @@ import com.mongodb.client.result.UpdateResult;
* @author Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont * @author Thomas Darimont
* @author Mark Paluch * @author Mark Paluch
* @author Jorge Rodríguez
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.LENIENT)
@ -518,6 +525,33 @@ class AbstractMongoQueryUnitTests {
assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("fn", 1)); assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("fn", 1));
} }
@Test // GH-2971
void findShouldApplyReadPreference() {
createQueryForMethod("findWithReadPreferenceByFirstname", String.class).execute(new Object[] { "Jasna" });
ArgumentCaptor<Query> 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<Query> 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) { private MongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
return createQueryForMethod(Repo.class, methodName, paramTypes); return createQueryForMethod(Repo.class, methodName, paramTypes);
} }
@ -588,6 +622,11 @@ class AbstractMongoQueryUnitTests {
} }
} }
@ReadPreference(
value = "primaryPreferred",
tags = { @ReadPreferenceTag(name = "primary-local", value = "east"), @ReadPreferenceTag(name = "primary-pre", value = "west") },
maxStalenessSeconds = 20
)
private interface Repo extends MongoRepository<Person, Long> { private interface Repo extends MongoRepository<Person, Long> {
List<Person> deleteByLastname(String lastname); List<Person> deleteByLastname(String lastname);
@ -643,6 +682,13 @@ class AbstractMongoQueryUnitTests {
List<Person> findWithLimit(String firstname, Limit limit); List<Person> findWithLimit(String firstname, Limit limit);
List<Person> findWithSortAndLimit(String firstname, Sort sort, Limit limit); List<Person> findWithSortAndLimit(String firstname, Sort sort, Limit limit);
@ReadPreference(
value = "secondaryPreferred",
tags = { @ReadPreferenceTag(name = "local", value = "east"), @ReadPreferenceTag(name = "pre", value = "west") },
maxStalenessSeconds = 99
)
List<Person> findWithReadPreferenceByFirstname(String firstname);
} }
// DATAMONGO-1872 // DATAMONGO-1872

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

@ -19,6 +19,9 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import com.mongodb.MongoClientSettings; import com.mongodb.MongoClientSettings;
import com.mongodb.Tag;
import com.mongodb.TagSet;
import com.mongodb.TaggableReadPreference;
import com.mongodb.client.result.UpdateResult; import com.mongodb.client.result.UpdateResult;
import org.bson.codecs.configuration.CodecRegistry; import org.bson.codecs.configuration.CodecRegistry;
import org.springframework.data.mongodb.core.ReactiveUpdateOperation.TerminatingUpdate; import org.springframework.data.mongodb.core.ReactiveUpdateOperation.TerminatingUpdate;
@ -26,6 +29,8 @@ import org.springframework.data.mongodb.core.ReactiveUpdateOperation.ReactiveUpd
import org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithQuery; import org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithQuery;
import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.repository.Hint; 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 org.springframework.data.mongodb.repository.Update;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -33,6 +38,7 @@ import reactor.core.publisher.Mono;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.TimeUnit;
import org.bson.Document; import org.bson.Document;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -68,7 +74,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
* @currentRead Way of Kings - Brandon Sanderson * @author Jorge Rodríguez
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.LENIENT)
@ -263,6 +269,19 @@ class AbstractReactiveMongoQueryUnitTests {
assertThat(captor.getValue().getHint()).isEqualTo("idx-ln"); assertThat(captor.getValue().getHint()).isEqualTo("idx-ln");
} }
@Test // GH-2971
void findShouldApplyReadPreference() {
createQueryForMethod("findWithReadPreferenceByFirstname", String.class).executeBlocking(new Object[] { "Jasna" });
ArgumentCaptor<Query> 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) { private ReactiveMongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
return createQueryForMethod(Repo.class, methodName, paramTypes); return createQueryForMethod(Repo.class, methodName, paramTypes);
} }
@ -367,5 +386,12 @@ class AbstractReactiveMongoQueryUnitTests {
@Hint("idx-fn") @Hint("idx-fn")
void findWithHintByFirstname(String firstname); void findWithHintByFirstname(String firstname);
@ReadPreference(
value = "secondaryPreferred",
tags = { @ReadPreferenceTag(name = "local", value = "east"), @ReadPreferenceTag(name = "pre", value = "west") },
maxStalenessSeconds = 99
)
Flux<Person> findWithReadPreferenceByFirstname(String firstname);
} }
} }

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

@ -41,6 +41,7 @@ import org.springframework.data.mongodb.repository.Contact;
import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.Meta;
import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.mongodb.repository.Query; 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.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
@ -52,6 +53,7 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat
* @author Oliver Gierke * @author Oliver Gierke
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
* @author Jorge Rodríguez
*/ */
public class MongoQueryMethodUnitTests { public class MongoQueryMethodUnitTests {
@ -311,6 +313,43 @@ public class MongoQueryMethodUnitTests {
assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT"); 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 { private MongoQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters) throws Exception {
Method method = repository.getMethod(name, parameters); Method method = repository.getMethod(name, parameters);
@ -318,6 +357,7 @@ public class MongoQueryMethodUnitTests {
return new MongoQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context); return new MongoQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context);
} }
@ReadPreference(value = "primaryPreferred")
interface PersonRepository extends Repository<User, Long> { interface PersonRepository extends Repository<User, Long> {
// Misses Pageable // Misses Pageable
@ -381,6 +421,16 @@ public class MongoQueryMethodUnitTests {
@Collation("de_AT") @Collation("de_AT")
@Query(collation = "en_US") @Query(collation = "en_US")
List<User> findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname); List<User> findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname);
@ReadPreference("secondaryPreferred")
List<User> findWithReadPreferenceFromAtReadPreferenceByFirstname(String firstname);
@Query(readPreference = "secondaryPreferred")
List<User> findWithReadPreferenceFromAtQueryByFirstname(String firstname);
@ReadPreference("secondaryPreferred")
@Query(readPreference = "primaryPreferred")
List<User> findWithMultipleReadPreferencesFromAtQueryAndAtReadPreferenceByFirstname(String firstname);
} }
interface SampleRepository extends Repository<Contact, Long> { interface SampleRepository extends Repository<Contact, Long> {

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

@ -21,6 +21,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.mongodb.core.annotation.Collation; import org.springframework.data.mongodb.core.annotation.Collation;
import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.mongodb.repository.ReadPreference;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -53,6 +54,7 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat
* *
* @author Mark Paluch * @author Mark Paluch
* @author Christoph Strobl * @author Christoph Strobl
* @author Jorge Rodríguez
*/ */
public class ReactiveMongoQueryMethodUnitTests { public class ReactiveMongoQueryMethodUnitTests {
@ -226,6 +228,43 @@ public class ReactiveMongoQueryMethodUnitTests {
assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT"); 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) private ReactiveMongoQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters)
throws Exception { throws Exception {
@ -234,6 +273,7 @@ public class ReactiveMongoQueryMethodUnitTests {
return new ReactiveMongoQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context); return new ReactiveMongoQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context);
} }
@ReadPreference(value = "primaryPreferred")
interface PersonRepository extends Repository<User, Long> { interface PersonRepository extends Repository<User, Long> {
Mono<Person> findMonoByLastname(String lastname, Pageable pageRequest); Mono<Person> findMonoByLastname(String lastname, Pageable pageRequest);
@ -277,6 +317,16 @@ public class ReactiveMongoQueryMethodUnitTests {
@Collation("de_AT") @Collation("de_AT")
@Query(collation = "en_US") @Query(collation = "en_US")
List<User> findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname); List<User> findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname(String firstname);
@ReadPreference("secondaryPreferred")
Flux<User> findWithReadPreferenceFromAtReadPreferenceByFirstname(String firstname);
@Query(readPreference = "secondaryPreferred")
Flux<User> findWithReadPreferenceFromAtQueryByFirstname(String firstname);
@ReadPreference("secondaryPreferred")
@Query(readPreference = "primaryPreferred")
Flux<User> findWithMultipleReadPreferencesFromAtQueryAndAtReadPreferenceByFirstname(String firstname);
} }
interface SampleRepository extends Repository<Contact, Long> { interface SampleRepository extends Repository<Contact, Long> {

30
src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc

@ -824,3 +824,33 @@ interface GameRepository extends Repository<Game, String> {
<2> Instead of `@Query(collation=...)`. <2> Instead of `@Query(collation=...)`.
<3> Favors `@Collation` over meta usage. <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<Person, String> {
@ReadPreference(value = "secondaryPreferred") <2>
List<Person> findWithReadPreferenceAnnotationByLastname(String lastname);
@Query(readPreference = "nearest") <3>
List<Person> findWithReadPreferenceAtTagByFirstname(String firstname);
List<Person> 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.
====

Loading…
Cancel
Save