diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java index 29ad75f14..42cd40c75 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java @@ -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.
+ * {@code @Aggregation(pipeline = { ... }, readPreference = "secondary")} can be used as shortcut for: + * + *
+	 * @@Aggregation(pipeline = { ... })
+	 * @ReadPreference("secondary")
+	 * List<PersonAggregate> groupByLastnameAnd(String property);
+	 * 
+ * + * @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/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java index a3928e40a..05d741ddd 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 @@ -157,7 +157,7 @@ public @interface Query { *
 	 * @Query(...)
 	 * @ReadPreference("secondary")
-	 * List<User> findAllByLastname(String collation);
+	 * List<User> findAllByLastname(String lastname);
 	 * 
* * @return the index name. 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 index 2ba4e32fd..0def67a22 100644 --- 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 @@ -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; } 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 deleted file mode 100644 index 5fae877ee..000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ReadPreferenceTag.java +++ /dev/null @@ -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(); -} 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 0913a9fb4..b1e0b3647 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 @@ -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) { 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 51301697e..d074a3803 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 @@ -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())); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java index e57286f76..4e6390c5e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java @@ -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. * 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 f4f30a5a9..68ba0a742 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,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 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 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 Optional doFindAnnotation(Class annotationType) { - return (Optional) this.annotationCache.computeIfAbsent(annotationType, it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it))); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java index fd40440c9..adffbe5fe 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java @@ -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) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java index 3f118d0fa..1a65e14db 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java @@ -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(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index 2d16be180..f768d2873 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -127,7 +127,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { MongoEntityInformation entityInformation = getEntityInformation(information.getDomainType(), information); - return getTargetRepositoryViaReflection(information, entityInformation, operations); + return getTargetRepositoryViaReflection(information, information, entityInformation, operations); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index 120e1eff9..b8dd2cc99 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -114,7 +114,7 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup MongoEntityInformation entityInformation = getEntityInformation(information.getDomainType(), information); - return getTargetRepositoryViaReflection(information, entityInformation, operations); + return getTargetRepositoryViaReflection(information, information, entityInformation, operations); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java index 78cfd2c98..077c84291 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java @@ -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 implements MongoRepository { - private final MongoOperations mongoOperations; + private final @Nullable RepositoryMetadata repositoryMetadata; private final MongoEntityInformation entityInformation; - + private final MongoOperations mongoOperations; + private final Lazy readPreference; /** * Creates a new {@link SimpleMongoRepository} for the given {@link MongoEntityInformation} and {@link MongoTemplate}. * @@ -72,12 +78,34 @@ public class SimpleMongoRepository implements MongoRepository { * @param mongoOperations must not be {@literal null}. */ public SimpleMongoRepository(MongoEntityInformation 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 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 implements MongoRepository { 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 implements MongoRepository { 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 implements MongoRepository { 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 implements MongoRepository { Query query = new Query(new Criteria().alike(example)) // .collation(entityInformation.getCollation()).with(pageable); // + readPreference.getOptional().ifPresent(query::withReadPreference); List list = mongoOperations.find(query, example.getProbeType(), entityInformation.getCollectionName()); @@ -337,7 +369,9 @@ public class SimpleMongoRepository implements MongoRepository { private Query getIdQuery(Iterable 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 Collection toCollection(Iterable ids) { @@ -351,6 +385,7 @@ public class SimpleMongoRepository implements MongoRepository { return Collections.emptyList(); } + readPreference.getOptional().ifPresent(query::withReadPreference); return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -440,6 +475,8 @@ public class SimpleMongoRepository implements MongoRepository { 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 implements MongoRepository { } } - } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java index 69b592468..f1db095f1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java @@ -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 implements ReactiveMongoRepository { + private final @Nullable RepositoryMetadata repositoryMetadata; private final MongoEntityInformation entityInformation; private final ReactiveMongoOperations mongoOperations; + private final Lazy readPreference; public SimpleReactiveMongoRepository(MongoEntityInformation 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 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 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 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 implement } private Flux findAll(Query query) { + + readPreference.getOptional().ifPresent(query::withReadPreference); return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -479,6 +514,8 @@ public class SimpleReactiveMongoRepository 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()) 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 7c416f5ca..06b4522fc 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,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 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 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 { List deleteByLastname(String lastname); @@ -683,11 +668,7 @@ class AbstractMongoQueryUnitTests { List 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 findWithReadPreferenceByFirstname(String firstname); } 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 df882b642..c0c07cdc6 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 @@ -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 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 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 53ae7a404..866391300 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 @@ -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 findByAggregationWithCollation(); + @Aggregation(pipeline = "{'$group': { _id: '$templateId', maxVersion : { $max : '$version'} } }", readPreference = "secondaryPreferred") + List findByAggregationWithReadPreference(); + void findAndUpdateBy(String firstname, Update update); void findAndUpdateBy(String firstname, UpdateDefinition update); 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 6122da17b..0b52bcb44 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 @@ -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 findByAggregationWithCollation(); + @Aggregation(pipeline = "{'$group': { _id: '$templateId', maxVersion : { $max : '$version'} } }", readPreference = "secondaryPreferred") + Flux findByAggregationWithReadPreference(); + @Collation("en_US") List findWithCollationFromAtCollationByFirstname(String firstname); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java index a54cb67db..c8f432fbd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java @@ -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 outSkipResult(); + + @Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT }, readPreference = "secondaryPreferred") + Mono withReadPreference(); } static class PersonAggregate { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java index 26fc04c2e..4c9b49947 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java @@ -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 { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java index 72ea5366a..a8ac1881a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java @@ -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> findCall) { + + RepositoryMetadata repositoryMetadata = mock(RepositoryMetadata.class); + doReturn(TestRepositoryWithReadPreference.class).when(repositoryMetadata).getRepositoryInterface(); + repository = new SimpleMongoRepository<>(repositoryMetadata, entityInformation, mongoOperations); + + findCall.accept(repository); + + ArgumentCaptor 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 = 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 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 = ArgumentCaptor.forClass(Query.class); + verify(finder).matching(query.capture()); + + assertThat(query.getValue().getReadPreference()).isEqualTo(com.mongodb.ReadPreference.secondaryPreferred()); + } + + private static Stream findAllCalls() { + + Consumer> findAll = SimpleMongoRepository::findAll; + Consumer> findAllWithSort = repo -> repo.findAll(Sort.by("age")); + Consumer> findAllWithPage = repo -> repo + .findAll(PageRequest.of(1, 20, Sort.by("age"))); + Consumer> findAllWithExample = repo -> repo + .findAll(Example.of(new TestDummy())); + Consumer> findAllWithExampleAndSort = repo -> repo + .findAll(Example.of(new TestDummy()), Sort.by("age")); + Consumer> 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 { + + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java index 04a54c686..206a025ca 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java @@ -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, Flux> 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 = 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 = 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 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 = ArgumentCaptor.forClass(Query.class); + verify(finder).matching(query.capture()); + + assertThat(query.getValue().getReadPreference()).isEqualTo(com.mongodb.ReadPreference.secondaryPreferred()); + } + + private static Stream findAllCalls() { + + Function, Flux> findAll = SimpleReactiveMongoRepository::findAll; + Function, Flux> findAllWithSort = repo -> repo + .findAll(Sort.by("age")); + Function, Flux> findAllWithExample = repo -> repo + .findAll(Example.of(new TestDummy())); + Function, Flux> 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 { + + } + } 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 129da7b75..fa9331136 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 @@ -827,21 +827,17 @@ interface GameRepository extends Repository { == 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 { - @ReadPreference(value = "secondaryPreferred") <2> + @ReadPreference("secondaryPreferred") <2> List findWithReadPreferenceAnnotationByLastname(String lastname); @Query(readPreference = "nearest") <3> @@ -849,8 +845,13 @@ public interface PersonRepository extends CrudRepository { 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` +<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`. +====