Detect ReadPreference for annotated aggregations and inherited find methods.

This commit reduces the API surface of the ReadPreference annotation leaving fine grained control such as Tags to the Template API.
Next to supporting read preference for annotated queries we now also make sure to apply read preferences for annotated aggregation methods as well as predefined finder methods running queries.

See: #2971
Original Pull Request: #4503
This commit is contained in:
Christoph Strobl
2023-09-27 14:55:06 +02:00
parent 16b97a26cc
commit 715231e9ef
23 changed files with 415 additions and 166 deletions

View File

@@ -44,6 +44,7 @@ import org.springframework.data.mongodb.core.annotation.Collation;
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
@QueryAnnotation
@ReadPreference
public @interface Aggregation {
/**
@@ -127,4 +128,21 @@ public @interface Aggregation {
*/
@AliasFor(annotation = Collation.class, attribute = "value")
String collation() default "";
/**
* The mode of the read preference to use. <br />
* {@code @Aggregation(pipeline = { ... }, readPreference = "secondary")} can be used as shortcut for:
*
* <pre class="code">
* &#64;@Aggregation(pipeline = { ... })
* &#64;ReadPreference("secondary")
* List&lt;PersonAggregate&gt; groupByLastnameAnd(String property);
* </pre>
*
* @return the index name.
* @since 4.2
* @see ReadPreference#value()
*/
@AliasFor(annotation = ReadPreference.class, attribute = "value")
String readPreference() default "";
}

View File

@@ -157,7 +157,7 @@ public @interface Query {
* <pre class="code">
* &#64;Query(...)
* &#64;ReadPreference("secondary")
* List&lt;User&gt; findAllByLastname(String collation);
* List&lt;User&gt; findAllByLastname(String lastname);
* </pre>
*
* @return the index name.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2023 the original author or authors.
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import java.lang.annotation.Target;
* Annotation to declare read preference for repository and query.
*
* @author Jorge Rodríguez
* @author Christoph Strobl
* @since 4.2
*/
@Retention(RetentionPolicy.RUNTIME)
@@ -38,16 +39,5 @@ public @interface ReadPreference {
*/
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;
}

View File

@@ -1,37 +0,0 @@
/*
* 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();
}

View File

@@ -159,8 +159,8 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
if (!method.hasAnnotatedReadPreference()) {
return query;
}
return query.withReadPreference(method.getAnnotatedReadPreference());
return query.withReadPreference(com.mongodb.ReadPreference.valueOf(method.getAnnotatedReadPreference()));
}
private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery<?> operation) {

View File

@@ -164,7 +164,6 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
query = applyHintIfPresent(query);
query = applyAnnotatedReadPreferenceIfPresent(query);
FindWithQuery<?> find = typeToRead == null //
? findOperationWithProjection //
: findOperationWithProjection.as(typeToRead);
@@ -303,7 +302,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
return query;
}
return query.withReadPreference(method.getAnnotatedReadPreference());
return query.withReadPreference(com.mongodb.ReadPreference.valueOf(method.getAnnotatedReadPreference()));
}
/**

View File

@@ -37,6 +37,8 @@ import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import com.mongodb.ReadPreference;
/**
* Internal utility class to help avoid duplicate code required in both the reactive and the sync {@link Aggregation}
* support offered by repositories.
@@ -117,6 +119,22 @@ abstract class AggregationUtils {
return builder.hint(queryMethod.getAnnotatedHint());
}
/**
* If present apply the preference from the {@link org.springframework.data.mongodb.repository.ReadPreference} annotation.
*
* @param builder must not be {@literal null}.
* @return never {@literal null}.
* @since 4.2
*/
static AggregationOptions.Builder applyReadPreference(AggregationOptions.Builder builder, MongoQueryMethod queryMethod) {
if (!queryMethod.hasAnnotatedReadPreference()) {
return builder;
}
return builder.readPreference(ReadPreference.valueOf(queryMethod.getAnnotatedReadPreference()));
}
/**
* Append {@code $sort} aggregation stage if {@link ConvertingParameterAccessor#getSort()} is present.
*

View File

@@ -22,11 +22,7 @@ import java.util.Arrays;
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;
@@ -322,7 +318,7 @@ public class MongoQueryMethod extends QueryMethod {
/**
* Check if the query method is decorated with an non empty {@link Query#collation()}.
* Check if the query method is decorated with an non empty {@link ReadPreference}.
*
* @return true if method annotated with {@link Query} or {@link Aggregation} having a non-empty collation attribute.
* @since 4.2
@@ -334,44 +330,27 @@ public class MongoQueryMethod extends QueryMethod {
/**
* Get the {@link com.mongodb.ReadPreference} extracted from the {@link ReadPreference} annotation.
*
* @return the {@link ReadPreference()}.
* @return the name of 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() {
public String 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(
return doFindReadPreferenceAnnotation().map(ReadPreference::value).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.
* Get {@link com.mongodb.ReadPreference#getName() name} 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}
* @return the {@link 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
* {@link Aggregation#collation()}.
@@ -458,7 +437,6 @@ public class MongoQueryMethod extends QueryMethod {
@SuppressWarnings("unchecked")
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
return (Optional<A>) this.annotationCache.computeIfAbsent(annotationType,
it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it)));

View File

@@ -132,6 +132,7 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
expressionParser, evaluationContextProvider);
AggregationUtils.applyMeta(builder, method);
AggregationUtils.applyHint(builder, method);
AggregationUtils.applyReadPreference(builder, method);
TypeInformation<?> returnType = method.getReturnType();
if (returnType.getComponentType() != null) {

View File

@@ -181,6 +181,7 @@ public class StringBasedAggregation extends AbstractMongoQuery {
expressionParser, evaluationContextProvider);
AggregationUtils.applyMeta(builder, method);
AggregationUtils.applyHint(builder, method);
AggregationUtils.applyReadPreference(builder, method);
if (ReflectionUtils.isVoid(method.getReturnType().getType()) && pipeline.isOutOrMerge()) {
builder.skipOutput();

View File

@@ -127,7 +127,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport {
MongoEntityInformation<?, Serializable> entityInformation = getEntityInformation(information.getDomainType(),
information);
return getTargetRepositoryViaReflection(information, entityInformation, operations);
return getTargetRepositoryViaReflection(information, information, entityInformation, operations);
}
@Override

View File

@@ -114,7 +114,7 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup
MongoEntityInformation<?, Serializable> entityInformation = getEntityInformation(information.getDomainType(),
information);
return getTargetRepositoryViaReflection(information, entityInformation, operations);
return getTargetRepositoryViaReflection(information, information, entityInformation, operations);
}
@Override

View File

@@ -27,6 +27,8 @@ import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.mongodb.ReadPreference;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
@@ -38,11 +40,14 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.ExecutableFindOperation;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.ReadPreferenceAware;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.StreamUtils;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
@@ -62,9 +67,10 @@ import com.mongodb.client.result.DeleteResult;
*/
public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
private final MongoOperations mongoOperations;
private final @Nullable RepositoryMetadata repositoryMetadata;
private final MongoEntityInformation<T, ID> entityInformation;
private final MongoOperations mongoOperations;
private final Lazy<ReadPreference> readPreference;
/**
* Creates a new {@link SimpleMongoRepository} for the given {@link MongoEntityInformation} and {@link MongoTemplate}.
*
@@ -72,12 +78,34 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
* @param mongoOperations must not be {@literal null}.
*/
public SimpleMongoRepository(MongoEntityInformation<T, ID> metadata, MongoOperations mongoOperations) {
this(null, metadata, mongoOperations);
}
/**
* Creates a new {@link SimpleMongoRepository} for the given {@link MongoEntityInformation} and {@link MongoTemplate}.
*
* @param repositoryMetadata
* @param metadata must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
* @since 4.2
*/
public SimpleMongoRepository(@Nullable RepositoryMetadata repositoryMetadata, MongoEntityInformation<T, ID> metadata, MongoOperations mongoOperations) {
Assert.notNull(metadata, "MongoEntityInformation must not be null");
Assert.notNull(mongoOperations, "MongoOperations must not be null");
this.repositoryMetadata = repositoryMetadata;
this.entityInformation = metadata;
this.mongoOperations = mongoOperations;
this.readPreference = repositoryMetadata == null ? Lazy.empty() : Lazy.of(() -> {
org.springframework.data.mongodb.repository.ReadPreference preference = AnnotatedElementUtils.findMergedAnnotation(repositoryMetadata.getRepositoryInterface(), org.springframework.data.mongodb.repository.ReadPreference.class);
if (preference == null) {
return null;
}
return ReadPreference.valueOf(preference.value());
}
);
}
// -------------------------------------------------------------------------
@@ -213,7 +241,8 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
Assert.notNull(sort, "Sort must not be null");
return findAll(new Query().with(sort));
Query query = new Query().with(sort);
return findAll(query);
}
// -------------------------------------------------------------------------
@@ -253,6 +282,7 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
Query query = new Query(new Criteria().alike(example)) //
.collation(entityInformation.getCollation());
readPreference.getOptional().ifPresent(query::withReadPreference);
return Optional
.ofNullable(mongoOperations.findOne(query, example.getProbeType(), entityInformation.getCollectionName()));
@@ -272,6 +302,7 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
Query query = new Query(new Criteria().alike(example)) //
.collation(entityInformation.getCollation()) //
.with(sort);
readPreference.getOptional().ifPresent(query::withReadPreference);
return mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName());
}
@@ -284,6 +315,7 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
Query query = new Query(new Criteria().alike(example)) //
.collation(entityInformation.getCollation()).with(pageable); //
readPreference.getOptional().ifPresent(query::withReadPreference);
List<S> list = mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName());
@@ -337,7 +369,9 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
private Query getIdQuery(Iterable<? extends ID> ids) {
return new Query(new Criteria(entityInformation.getIdAttribute()).in(toCollection(ids)));
Query query = new Query(new Criteria(entityInformation.getIdAttribute()).in(toCollection(ids)));
readPreference.getOptional().ifPresent(query::withReadPreference);
return query;
}
private static <E> Collection<E> toCollection(Iterable<E> ids) {
@@ -351,6 +385,7 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
return Collections.emptyList();
}
readPreference.getOptional().ifPresent(query::withReadPreference);
return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName());
}
@@ -440,6 +475,8 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
query.fields().include(getFieldsToInclude().toArray(new String[0]));
}
readPreference.getOptional().ifPresent(query::withReadPreference);
query = queryCustomizer.apply(query);
return mongoOperations.query(getPredicate().getProbeType()).inCollection(entityInformation.getCollectionName())
@@ -447,5 +484,4 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
}
}
}

View File

@@ -17,6 +17,12 @@ package org.springframework.data.mongodb.repository.support;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import com.mongodb.ReadPreference;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -63,17 +69,42 @@ import com.mongodb.client.result.DeleteResult;
*/
public class SimpleReactiveMongoRepository<T, ID extends Serializable> implements ReactiveMongoRepository<T, ID> {
private final @Nullable RepositoryMetadata repositoryMetadata;
private final MongoEntityInformation<T, ID> entityInformation;
private final ReactiveMongoOperations mongoOperations;
private final Lazy<ReadPreference> readPreference;
public SimpleReactiveMongoRepository(MongoEntityInformation<T, ID> entityInformation,
ReactiveMongoOperations mongoOperations) {
this(null, entityInformation, mongoOperations);
}
/**
* Creates a new {@link SimpleReactiveMongoRepository} for the given {@link MongoEntityInformation} and {@link MongoTemplate}.
*
* @param repositoryMetadata
* @param entityInformation must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
* @since 4.2
*/
public SimpleReactiveMongoRepository(@Nullable RepositoryMetadata repositoryMetadata, MongoEntityInformation<T, ID> entityInformation,
ReactiveMongoOperations mongoOperations) {
Assert.notNull(entityInformation, "EntityInformation must not be null");
Assert.notNull(mongoOperations, "MongoOperations must not be null");
this.repositoryMetadata = repositoryMetadata;
this.entityInformation = entityInformation;
this.mongoOperations = mongoOperations;
this.readPreference = repositoryMetadata == null ? Lazy.empty() : Lazy.of(() -> {
org.springframework.data.mongodb.repository.ReadPreference preference = AnnotatedElementUtils.findMergedAnnotation(repositoryMetadata.getRepositoryInterface(), org.springframework.data.mongodb.repository.ReadPreference.class);
if (preference == null) {
return null;
}
return ReadPreference.valueOf(preference.value());
}
);
}
// -------------------------------------------------------------------------
@@ -312,6 +343,7 @@ public class SimpleReactiveMongoRepository<T, ID extends Serializable> implement
Query query = new Query(new Criteria().alike(example)) //
.collation(entityInformation.getCollation()) //
.limit(2);
readPreference.getOptional().ifPresent(query::withReadPreference);
return mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName()).buffer(2)
.map(vals -> {
@@ -340,6 +372,7 @@ public class SimpleReactiveMongoRepository<T, ID extends Serializable> implement
Query query = new Query(new Criteria().alike(example)) //
.collation(entityInformation.getCollation()) //
.with(sort);
readPreference.getOptional().ifPresent(query::withReadPreference);
return mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName());
}
@@ -394,6 +427,8 @@ public class SimpleReactiveMongoRepository<T, ID extends Serializable> implement
}
private Flux<T> findAll(Query query) {
readPreference.getOptional().ifPresent(query::withReadPreference);
return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName());
}
@@ -479,6 +514,8 @@ public class SimpleReactiveMongoRepository<T, ID extends Serializable> implement
query.fields().include(getFieldsToInclude().toArray(new String[0]));
}
readPreference.getOptional().ifPresent(query::withReadPreference);
query = queryCustomizer.apply(query);
return mongoOperations.query(getPredicate().getProbeType()).inCollection(entityInformation.getCollectionName())

View File

@@ -23,11 +23,7 @@ import java.lang.reflect.Method;
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;
@@ -70,7 +66,6 @@ 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;
@@ -532,10 +527,7 @@ class AbstractMongoQueryUnitTests {
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);
assertThat(captor.getValue().getReadPreference()).isEqualTo(com.mongodb.ReadPreference.secondaryPreferred());
}
@Test // GH-2971
@@ -545,10 +537,7 @@ class AbstractMongoQueryUnitTests {
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);
assertThat(captor.getValue().getReadPreference()).isEqualTo(com.mongodb.ReadPreference.primaryPreferred());
}
@@ -622,11 +611,7 @@ class AbstractMongoQueryUnitTests {
}
}
@ReadPreference(
value = "primaryPreferred",
tags = { @ReadPreferenceTag(name = "primary-local", value = "east"), @ReadPreferenceTag(name = "primary-pre", value = "west") },
maxStalenessSeconds = 20
)
@ReadPreference(value = "primaryPreferred")
private interface Repo extends MongoRepository<Person, Long> {
List<Person> deleteByLastname(String lastname);
@@ -683,11 +668,7 @@ class AbstractMongoQueryUnitTests {
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
)
@ReadPreference(value = "secondaryPreferred")
List<Person> findWithReadPreferenceByFirstname(String firstname);
}

View File

@@ -18,29 +18,15 @@ package org.springframework.data.mongodb.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.mongodb.MongoClientSettings;
import com.mongodb.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;
import org.springframework.data.mongodb.core.ReactiveUpdateOperation.ReactiveUpdate;
import org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithQuery;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.repository.Hint;
import org.springframework.data.mongodb.repository.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;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import org.bson.Document;
import org.bson.codecs.configuration.CodecRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -50,11 +36,13 @@ import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.data.mongodb.core.Person;
import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.ReactiveFindOperation.ReactiveFind;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.ReactiveUpdateOperation.ReactiveUpdate;
import org.springframework.data.mongodb.core.ReactiveUpdateOperation.TerminatingUpdate;
import org.springframework.data.mongodb.core.ReactiveUpdateOperation.UpdateWithQuery;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
@@ -62,13 +50,20 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.repository.Hint;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.data.mongodb.repository.ReadPreference;
import org.springframework.data.mongodb.repository.Update;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.result.UpdateResult;
/**
* Unit tests for {@link AbstractReactiveMongoQuery}.
*
@@ -276,10 +271,7 @@ class AbstractReactiveMongoQueryUnitTests {
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);
assertThat(captor.getValue().getReadPreference()).isEqualTo(com.mongodb.ReadPreference.secondaryPreferred());
}
private ReactiveMongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
@@ -387,11 +379,7 @@ class AbstractReactiveMongoQueryUnitTests {
@Hint("idx-fn")
void findWithHintByFirstname(String firstname);
@ReadPreference(
value = "secondaryPreferred",
tags = { @ReadPreferenceTag(name = "local", value = "east"), @ReadPreferenceTag(name = "pre", value = "west") },
maxStalenessSeconds = 99
)
@ReadPreference(value = "secondaryPreferred")
Flux<Person> findWithReadPreferenceByFirstname(String firstname);
}
}

View File

@@ -319,7 +319,7 @@ public class MongoQueryMethodUnitTests {
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithReadPreferenceFromAtReadPreferenceByFirstname", String.class);
assertThat(method.hasAnnotatedReadPreference()).isTrue();
assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred");
assertThat(method.getAnnotatedReadPreference()).isEqualTo("secondaryPreferred");
}
@Test // GH-2971
@@ -328,7 +328,7 @@ public class MongoQueryMethodUnitTests {
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithReadPreferenceFromAtQueryByFirstname", String.class);
assertThat(method.hasAnnotatedReadPreference()).isTrue();
assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred");
assertThat(method.getAnnotatedReadPreference()).isEqualTo("secondaryPreferred");
}
@Test // GH-2971
@@ -337,8 +337,7 @@ public class MongoQueryMethodUnitTests {
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithMultipleReadPreferencesFromAtQueryAndAtReadPreferenceByFirstname", String.class);
assertThat(method.hasAnnotatedReadPreference()).isTrue();
assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred");
assertThat(method.getAnnotatedReadPreference()).isEqualTo("secondaryPreferred");
}
@Test // GH-2971
@@ -347,7 +346,16 @@ public class MongoQueryMethodUnitTests {
MongoQueryMethod method = queryMethod(PersonRepository.class, "deleteByUserName", String.class);
assertThat(method.hasAnnotatedReadPreference()).isTrue();
assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("primaryPreferred");
assertThat(method.getAnnotatedReadPreference()).isEqualTo("primaryPreferred");
}
@Test // GH-2971
void detectsReadPreferenceForAggregation() throws Exception {
MongoQueryMethod method = queryMethod(PersonRepository.class, "findByAggregationWithReadPreference");
assertThat(method.hasAnnotatedReadPreference()).isTrue();
assertThat(method.getAnnotatedReadPreference()).isEqualTo("secondaryPreferred");
}
private MongoQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters) throws Exception {
@@ -406,6 +414,9 @@ public class MongoQueryMethodUnitTests {
collation = "de_AT")
List<User> findByAggregationWithCollation();
@Aggregation(pipeline = "{'$group': { _id: '$templateId', maxVersion : { $max : '$version'} } }", readPreference = "secondaryPreferred")
List<User> findByAggregationWithReadPreference();
void findAndUpdateBy(String firstname, Update update);
void findAndUpdateBy(String firstname, UpdateDefinition update);

View File

@@ -17,11 +17,7 @@ package org.springframework.data.mongodb.repository.query;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
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 org.springframework.data.mongodb.repository.query.MongoQueryMethodUnitTests.PersonRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -29,7 +25,8 @@ import java.lang.reflect.Method;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -38,12 +35,15 @@ import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.User;
import org.springframework.data.mongodb.core.annotation.Collation;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.Address;
import org.springframework.data.mongodb.repository.Aggregation;
import org.springframework.data.mongodb.repository.Contact;
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;
@@ -222,7 +222,7 @@ public class ReactiveMongoQueryMethodUnitTests {
@Test // GH-3002
void annotatedCollationClashSelectsAtCollationAnnotationValue() throws Exception {
ReactiveMongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname", String.class);
ReactiveMongoQueryMethod method = queryMethod(PersonRepository.class, "findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname", String.class);
assertThat(method.hasAnnotatedCollation()).isTrue();
assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT");
@@ -232,37 +232,46 @@ public class ReactiveMongoQueryMethodUnitTests {
@Test // GH-2971
void readsReadPreferenceAtQueryAnnotation() throws Exception {
MongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithReadPreferenceFromAtReadPreferenceByFirstname", String.class);
ReactiveMongoQueryMethod method = queryMethod(PersonRepository.class, "findWithReadPreferenceFromAtReadPreferenceByFirstname", String.class);
assertThat(method.hasAnnotatedReadPreference()).isTrue();
assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred");
assertThat(method.getAnnotatedReadPreference()).isEqualTo("secondaryPreferred");
}
@Test // GH-2971
void readsReadPreferenceFromAtQueryAnnotation() throws Exception {
MongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithReadPreferenceFromAtQueryByFirstname", String.class);
ReactiveMongoQueryMethod method = queryMethod(PersonRepository.class, "findWithReadPreferenceFromAtQueryByFirstname", String.class);
assertThat(method.hasAnnotatedReadPreference()).isTrue();
assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred");
assertThat(method.getAnnotatedReadPreference()).isEqualTo("secondaryPreferred");
}
@Test // GH-2971
void annotatedReadPreferenceClashSelectsAtReadPreferenceAnnotationValue() throws Exception {
MongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findWithMultipleReadPreferencesFromAtQueryAndAtReadPreferenceByFirstname", String.class);
ReactiveMongoQueryMethod method = queryMethod(PersonRepository.class, "findWithMultipleReadPreferencesFromAtQueryAndAtReadPreferenceByFirstname", String.class);
assertThat(method.hasAnnotatedReadPreference()).isTrue();
assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("secondaryPreferred");
assertThat(method.getAnnotatedReadPreference()).isEqualTo("secondaryPreferred");
}
@Test // GH-2971
void readsReadPreferenceAtRepositoryAnnotation() throws Exception {
MongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "deleteByUserName", String.class);
ReactiveMongoQueryMethod method = queryMethod(PersonRepository.class, "deleteByUserName", String.class);
assertThat(method.hasAnnotatedReadPreference()).isTrue();
assertThat(method.getAnnotatedReadPreference().getName()).isEqualTo("primaryPreferred");
assertThat(method.getAnnotatedReadPreference()).isEqualTo("primaryPreferred");
}
@Test // GH-2971
void detectsReadPreferenceForAggregation() throws Exception {
ReactiveMongoQueryMethod method = queryMethod(MongoQueryMethodUnitTests.PersonRepository.class, "findByAggregationWithReadPreference");
assertThat(method.hasAnnotatedReadPreference()).isTrue();
assertThat(method.getAnnotatedReadPreference()).isEqualTo("secondaryPreferred");
}
private ReactiveMongoQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters)
@@ -308,6 +317,9 @@ public class ReactiveMongoQueryMethodUnitTests {
collation = "de_AT")
Flux<User> findByAggregationWithCollation();
@Aggregation(pipeline = "{'$group': { _id: '$templateId', maxVersion : { $max : '$version'} } }", readPreference = "secondaryPreferred")
Flux<User> findByAggregationWithReadPreference();
@Collation("en_US")
List<User> findWithCollationFromAtCollationByFirstname(String firstname);

View File

@@ -60,6 +60,8 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import com.mongodb.ReadPreference;
/**
* Unit tests for {@link ReactiveStringBasedAggregation}.
*
@@ -211,6 +213,13 @@ public class ReactiveStringBasedAggregationUnitTests {
assertThat(skipResultsOf(invocation)).isFalse();
}
@Test // GH-2971
void aggregatePicksUpReadPreferenceFromAnnotation() {
AggregationInvocation invocation = executeAggregation("withReadPreference");
assertThat(readPreferenceOf(invocation)).isEqualTo(ReadPreference.secondaryPreferred());
}
private ReactiveStringBasedAggregation createAggregationForMethod(String name, Class<?>... parameters) {
Method method = ClassUtils.getMethod(SampleRepository.class, name, parameters);
@@ -250,6 +259,12 @@ public class ReactiveStringBasedAggregationUnitTests {
: false;
}
@Nullable
private ReadPreference readPreferenceOf(AggregationInvocation invocation) {
return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference()
: null;
}
private Class<?> targetTypeOf(AggregationInvocation invocation) {
return invocation.getTargetType();
}
@@ -287,6 +302,9 @@ public class ReactiveStringBasedAggregationUnitTests {
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT })
Mono<Void> outSkipResult();
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT }, readPreference = "secondaryPreferred")
Mono<PersonAggregate> withReadPreference();
}
static class PersonAggregate {

View File

@@ -68,6 +68,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import com.mongodb.MongoClientSettings;
import com.mongodb.ReadPreference;
/**
* Unit tests for {@link StringBasedAggregation}.
@@ -282,6 +283,13 @@ public class StringBasedAggregationUnitTests {
assertThat(skipResultsOf(invocation)).isFalse();
}
@Test // GH-2971
void aggregatePicksUpReadPreferenceFromAnnotation() {
AggregationInvocation invocation = executeAggregation("withReadPreference");
assertThat(readPreferenceOf(invocation)).isEqualTo(ReadPreference.secondaryPreferred());
}
private AggregationInvocation executeAggregation(String name, Object... args) {
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
@@ -335,6 +343,12 @@ public class StringBasedAggregationUnitTests {
: false;
}
@Nullable
private ReadPreference readPreferenceOf(AggregationInvocation invocation) {
return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference()
: null;
}
private Class<?> targetTypeOf(AggregationInvocation invocation) {
return invocation.getTargetType();
}
@@ -393,6 +407,9 @@ public class StringBasedAggregationUnitTests {
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT })
void outSkipResult();
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT }, readPreference = "secondaryPreferred")
void withReadPreference();
}
private interface UnsupportedRepository extends Repository<Person, Long> {

View File

@@ -19,20 +19,29 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.ReadPreference;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
/**
* @author Christoph Strobl
@@ -133,8 +142,91 @@ public class SimpleMongoRepositoryUnitTests {
assertThat(query.getValue().getCollation()).contains(collation);
}
@ParameterizedTest // GH-2971
@MethodSource("findAllCalls")
void shouldAddReadPreferenceToFindAllMethods(Consumer<SimpleMongoRepository<Object, Object>> findCall) {
RepositoryMetadata repositoryMetadata = mock(RepositoryMetadata.class);
doReturn(TestRepositoryWithReadPreference.class).when(repositoryMetadata).getRepositoryInterface();
repository = new SimpleMongoRepository<>(repositoryMetadata, entityInformation, mongoOperations);
findCall.accept(repository);
ArgumentCaptor<Query> query = ArgumentCaptor.forClass(Query.class);
verify(mongoOperations).find(query.capture(), any(), any());
assertThat(query.getValue().getReadPreference()).isEqualTo(com.mongodb.ReadPreference.secondaryPreferred());
}
@Test // GH-2971
void shouldAddReadPreferenceToFindOne() {
RepositoryMetadata repositoryMetadata = mock(RepositoryMetadata.class);
doReturn(TestRepositoryWithReadPreference.class).when(repositoryMetadata).getRepositoryInterface();
repository = new SimpleMongoRepository<>(repositoryMetadata, entityInformation, mongoOperations);
repository.findOne(Example.of(new TestDummy()));
ArgumentCaptor<Query> query = ArgumentCaptor.forClass(Query.class);
verify(mongoOperations).findOne(query.capture(), any(), any());
assertThat(query.getValue().getReadPreference()).isEqualTo(com.mongodb.ReadPreference.secondaryPreferred());
}
@Test // GH-2971
void shouldAddReadPreferenceToFluentFetchable() {
RepositoryMetadata repositoryMetadata = mock(RepositoryMetadata.class);
doReturn(TestRepositoryWithReadPreference.class).when(repositoryMetadata).getRepositoryInterface();
ExecutableFind<Object> finder = mock(ExecutableFind.class);
when(mongoOperations.query(any())).thenReturn(finder);
when(finder.inCollection(any())).thenReturn(finder);
when(finder.matching(any(Query.class))).thenReturn(finder);
when(finder.as(any())).thenReturn(finder);
repository = new SimpleMongoRepository<>(repositoryMetadata, entityInformation, mongoOperations);
repository.findBy(Example.of(new TestDummy()), FetchableFluentQuery::all);
ArgumentCaptor<Query> query = ArgumentCaptor.forClass(Query.class);
verify(finder).matching(query.capture());
assertThat(query.getValue().getReadPreference()).isEqualTo(com.mongodb.ReadPreference.secondaryPreferred());
}
private static Stream<Arguments> findAllCalls() {
Consumer<SimpleMongoRepository<Object, Object>> findAll = SimpleMongoRepository::findAll;
Consumer<SimpleMongoRepository<Object, Object>> findAllWithSort = repo -> repo.findAll(Sort.by("age"));
Consumer<SimpleMongoRepository<Object, Object>> findAllWithPage = repo -> repo
.findAll(PageRequest.of(1, 20, Sort.by("age")));
Consumer<SimpleMongoRepository<Object, Object>> findAllWithExample = repo -> repo
.findAll(Example.of(new TestDummy()));
Consumer<SimpleMongoRepository<Object, Object>> findAllWithExampleAndSort = repo -> repo
.findAll(Example.of(new TestDummy()), Sort.by("age"));
Consumer<SimpleMongoRepository<Object, Object>> findAllWithExampleAndPage = repo -> repo
.findAll(Example.of(new TestDummy()), PageRequest.of(1, 20, Sort.by("age")));
return Stream.of(Arguments.of(findAll), //
Arguments.of(findAllWithSort), //
Arguments.of(findAllWithPage), //
Arguments.of(findAllWithExample), //
Arguments.of(findAllWithExampleAndSort), //
Arguments.of(findAllWithExampleAndPage));
}
static class TestDummy {
}
interface TestRepository {
}
@ReadPreference("secondaryPreferred")
interface TestRepositoryWithReadPreference {
}
}

View File

@@ -22,18 +22,28 @@ import static org.mockito.Mockito.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.function.Function;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.ReactiveFindOperation.ReactiveFind;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.ReadPreference;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.FluentQuery;
/**
* @author Christoph Strobl
@@ -136,8 +146,86 @@ class SimpleReactiveMongoRepositoryUnitTests {
assertThat(query.getValue().getCollation()).contains(collation);
}
@ParameterizedTest // GH-2971
@MethodSource("findAllCalls")
void shouldAddReadPreferenceToFindAllMethods(Function<SimpleReactiveMongoRepository<Object, String>, Flux<Object>> findCall) {
RepositoryMetadata repositoryMetadata = mock(RepositoryMetadata.class);
doReturn(TestRepositoryWithReadPreference.class).when(repositoryMetadata).getRepositoryInterface();
repository = new SimpleReactiveMongoRepository<>(repositoryMetadata, entityInformation, mongoOperations);
when(mongoOperations.find(any(), any(), any())).thenReturn(Flux.just("ok"));
findCall.apply(repository).subscribe();
ArgumentCaptor<Query> query = ArgumentCaptor.forClass(Query.class);
verify(mongoOperations).find(query.capture(), any(), any());
assertThat(query.getValue().getReadPreference()).isEqualTo(com.mongodb.ReadPreference.secondaryPreferred());
}
@Test // GH-2971
void shouldAddReadPreferenceToFindOne() {
RepositoryMetadata repositoryMetadata = mock(RepositoryMetadata.class);
doReturn(TestRepositoryWithReadPreference.class).when(repositoryMetadata).getRepositoryInterface();
repository = new SimpleReactiveMongoRepository<>(repositoryMetadata, entityInformation, mongoOperations);
when(mongoOperations.find(any(), any(), any())).thenReturn(Flux.just("ok"));
repository.findOne(Example.of(new SimpleMongoRepositoryUnitTests.TestDummy())).subscribe();
ArgumentCaptor<Query> query = ArgumentCaptor.forClass(Query.class);
verify(mongoOperations).find(query.capture(), any(), any());
assertThat(query.getValue().getReadPreference()).isEqualTo(com.mongodb.ReadPreference.secondaryPreferred());
}
@Test // GH-2971
void shouldAddReadPreferenceToFluentFetchable() {
RepositoryMetadata repositoryMetadata = mock(RepositoryMetadata.class);
doReturn(SimpleMongoRepositoryUnitTests.TestRepositoryWithReadPreference.class).when(repositoryMetadata)
.getRepositoryInterface();
ReactiveFind<Object> finder = mock(ReactiveFind.class);
when(mongoOperations.query(any())).thenReturn(finder);
when(finder.inCollection(any())).thenReturn(finder);
when(finder.matching(any(Query.class))).thenReturn(finder);
when(finder.as(any())).thenReturn(finder);
when(finder.all()).thenReturn(Flux.just("ok"));
repository = new SimpleReactiveMongoRepository<>(repositoryMetadata, entityInformation, mongoOperations);
repository.findBy(Example.of(new TestDummy()), FluentQuery.ReactiveFluentQuery::all).subscribe();
ArgumentCaptor<Query> query = ArgumentCaptor.forClass(Query.class);
verify(finder).matching(query.capture());
assertThat(query.getValue().getReadPreference()).isEqualTo(com.mongodb.ReadPreference.secondaryPreferred());
}
private static Stream<Arguments> findAllCalls() {
Function<SimpleReactiveMongoRepository<Object, String>, Flux<Object>> findAll = SimpleReactiveMongoRepository::findAll;
Function<SimpleReactiveMongoRepository<Object, String>, Flux<Object>> findAllWithSort = repo -> repo
.findAll(Sort.by("age"));
Function<SimpleReactiveMongoRepository<Object, String>, Flux<Object>> findAllWithExample = repo -> repo
.findAll(Example.of(new TestDummy()));
Function<SimpleReactiveMongoRepository<Object, String>, Flux<Object>> findAllWithExampleAndSort = repo -> repo
.findAll(Example.of(new TestDummy()), Sort.by("age"));
return Stream.of(Arguments.of(findAll), //
Arguments.of(findAllWithSort), //
Arguments.of(findAllWithExample), //
Arguments.of(findAllWithExampleAndSort));
}
private static class TestDummy {
}
@ReadPreference("secondaryPreferred")
interface TestRepositoryWithReadPreference {
}
}

View File

@@ -827,21 +827,17 @@ interface GameRepository extends Repository<Game, String> {
== Read Preferences
The `@ReadPreference` annotation allows you to configure MongoDB's ReadPreferences
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>
@ReadPreference("primaryPreferred") <1>
public interface PersonRepository extends CrudRepository<Person, String> {
@ReadPreference(value = "secondaryPreferred") <2>
@ReadPreference("secondaryPreferred") <2>
List<Person> findWithReadPreferenceAnnotationByLastname(String lastname);
@Query(readPreference = "nearest") <3>
@@ -849,8 +845,13 @@ public interface PersonRepository extends CrudRepository<Person, String> {
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`
<1> Configure read preference for all repository operations (including inherited, non custom implementation ones) 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.
====
[TIP]
====
The `MongoOperations` and `Query` API offer more fine grained control for `ReadPreference`.
====