From 715231e9ef2161e5d859ac57ceb1f4ea08128912 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 27 Sep 2023 14:55:06 +0200 Subject: [PATCH] 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 --- .../data/mongodb/repository/Aggregation.java | 18 ++++ .../data/mongodb/repository/Query.java | 2 +- .../mongodb/repository/ReadPreference.java | 14 +-- .../mongodb/repository/ReadPreferenceTag.java | 37 -------- .../repository/query/AbstractMongoQuery.java | 4 +- .../query/AbstractReactiveMongoQuery.java | 3 +- .../repository/query/AggregationUtils.java | 18 ++++ .../repository/query/MongoQueryMethod.java | 34 ++----- .../query/ReactiveStringBasedAggregation.java | 1 + .../query/StringBasedAggregation.java | 1 + .../support/MongoRepositoryFactory.java | 2 +- .../ReactiveMongoRepositoryFactory.java | 2 +- .../support/SimpleMongoRepository.java | 46 ++++++++- .../SimpleReactiveMongoRepository.java | 37 ++++++++ .../query/AbstractMongoQueryUnitTests.java | 27 +----- .../AbstractReactiveMongoQueryUnitTests.java | 38 +++----- .../query/MongoQueryMethodUnitTests.java | 21 ++++- .../ReactiveMongoQueryMethodUnitTests.java | 42 ++++++--- ...activeStringBasedAggregationUnitTests.java | 18 ++++ .../StringBasedAggregationUnitTests.java | 17 ++++ .../SimpleMongoRepositoryUnitTests.java | 94 ++++++++++++++++++- ...impleReactiveMongoRepositoryUnitTests.java | 88 +++++++++++++++++ .../mongodb/repositories/query-methods.adoc | 17 ++-- 23 files changed, 415 insertions(+), 166 deletions(-) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ReadPreferenceTag.java 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`. +====