diff --git a/pom.xml b/pom.xml index d864b2e4e6..9b46a7422c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.0-GH-4277-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 1b2a1390e6..32891d6ea9 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.0-GH-4277-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 8db8d798fb..8972e86bd6 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.0-GH-4277-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 9a57f7eb52..2646d9673c 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.0-GH-4277-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparer.java new file mode 100644 index 0000000000..bac4dbe746 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparer.java @@ -0,0 +1,61 @@ +/* + * 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. + * 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.core; + +import org.springframework.util.Assert; + +import com.mongodb.client.MongoCollection; + +/** + * Interface for functional preparation of a {@link MongoCollection}. + * + * @author Mark Paluch + * @since 4.1 + */ +public interface CollectionPreparer { + + /** + * Returns a preparer that always returns its input collection. + * + * @return a preparer that always returns its input collection. + */ + static CollectionPreparer identity() { + return it -> it; + } + + /** + * Prepare the {@code collection}. + * + * @param collection the collection to prepare. + * @return the prepared collection. + */ + T prepare(T collection); + + /** + * Returns a composed {@code CollectionPreparer} that first applies this preparer to the collection, and then applies + * the {@code after} preparer to the result. If evaluation of either function throws an exception, it is relayed to + * the caller of the composed function. + * + * @param after the collection preparer to apply after this function is applied. + * @return a composed {@code CollectionPreparer} that first applies this preparer and then applies the {@code after} + * preparer. + */ + default CollectionPreparer andThen(CollectionPreparer after) { + Assert.notNull(after, "After CollectionPreparer must not be null"); + return c -> after.prepare(prepare(c)); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java new file mode 100644 index 0000000000..d8e3eeabba --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java @@ -0,0 +1,182 @@ +/* + * 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. + * 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.core; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.bson.Document; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.client.MongoCollection; + +/** + * Support class for delegate implementations to apply {@link ReadConcern} and {@link ReadPreference} settings upon + * {@link CollectionPreparer preparing a collection}. + * + * @author Mark Paluch + * @since 4.1 + */ +class CollectionPreparerSupport implements ReadConcernAware, ReadPreferenceAware { + + private final List sources; + + private CollectionPreparerSupport(List sources) { + this.sources = sources; + } + + T doPrepare(T collection, Function concernAccessor, BiFunction concernFunction, + Function preferenceAccessor, BiFunction preferenceFunction) { + + T collectionToUse = collection; + + for (Object source : sources) { + if (source instanceof ReadConcernAware rca && rca.hasReadConcern()) { + + ReadConcern concern = rca.getReadConcern(); + if (concernAccessor.apply(collectionToUse) != concern) { + collectionToUse = concernFunction.apply(collectionToUse, concern); + } + break; + } + } + + for (Object source : sources) { + if (source instanceof ReadPreferenceAware rpa && rpa.hasReadPreference()) { + + ReadPreference preference = rpa.getReadPreference(); + if (preferenceAccessor.apply(collectionToUse) != preference) { + collectionToUse = preferenceFunction.apply(collectionToUse, preference); + } + break; + } + } + + return collectionToUse; + } + + @Override + public boolean hasReadConcern() { + + for (Object aware : sources) { + if (aware instanceof ReadConcernAware rca && rca.hasReadConcern()) { + return true; + } + } + + return false; + } + + @Override + public ReadConcern getReadConcern() { + + for (Object aware : sources) { + if (aware instanceof ReadConcernAware rca && rca.hasReadConcern()) { + return rca.getReadConcern(); + } + } + + return null; + } + + @Override + public boolean hasReadPreference() { + + for (Object aware : sources) { + if (aware instanceof ReadPreferenceAware rpa && rpa.hasReadPreference()) { + return true; + } + } + + return false; + } + + @Override + public ReadPreference getReadPreference() { + + for (Object aware : sources) { + if (aware instanceof ReadPreferenceAware rpa && rpa.hasReadPreference()) { + return rpa.getReadPreference(); + } + } + + return null; + } + + static class CollectionPreparerDelegate extends CollectionPreparerSupport + implements CollectionPreparer> { + + private CollectionPreparerDelegate(List sources) { + super(sources); + } + + public static CollectionPreparerDelegate of(ReadPreferenceAware... awares) { + return of((Object[]) awares); + } + + public static CollectionPreparerDelegate of(Object... mixedAwares) { + + if (mixedAwares.length == 1 && mixedAwares[0] instanceof CollectionPreparerDelegate) { + return (CollectionPreparerDelegate) mixedAwares[0]; + } + + return new CollectionPreparerDelegate(Arrays.asList(mixedAwares)); + } + + @Override + public MongoCollection prepare(MongoCollection collection) { + return doPrepare(collection, MongoCollection::getReadConcern, MongoCollection::withReadConcern, + MongoCollection::getReadPreference, MongoCollection::withReadPreference); + } + + } + + static class ReactiveCollectionPreparerDelegate extends CollectionPreparerSupport + implements CollectionPreparer> { + + private ReactiveCollectionPreparerDelegate(List sources) { + super(sources); + } + + public static ReactiveCollectionPreparerDelegate of(ReadPreferenceAware... awares) { + return of((Object[]) awares); + } + + public static ReactiveCollectionPreparerDelegate of(Object... mixedAwares) { + + if (mixedAwares.length == 1 && mixedAwares[0] instanceof CollectionPreparerDelegate) { + return (ReactiveCollectionPreparerDelegate) mixedAwares[0]; + } + + return new ReactiveCollectionPreparerDelegate(Arrays.asList(mixedAwares)); + } + + @Override + public com.mongodb.reactivestreams.client.MongoCollection prepare( + com.mongodb.reactivestreams.client.MongoCollection collection) { + return doPrepare(collection, // + com.mongodb.reactivestreams.client.MongoCollection::getReadConcern, + com.mongodb.reactivestreams.client.MongoCollection::withReadConcern, + com.mongodb.reactivestreams.client.MongoCollection::getReadPreference, + com.mongodb.reactivestreams.client.MongoCollection::withReadPreference); + } + + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java index 111492e3c7..1038ee2327 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java @@ -20,7 +20,6 @@ import java.util.stream.Stream; import org.bson.Document; - import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; @@ -168,7 +167,8 @@ private List doFind(@Nullable CursorPreparer preparer) { Document queryObject = query.getQueryObject(); Document fieldsObject = query.getFieldsObject(); - return template.doFind(getCollectionName(), queryObject, fieldsObject, domainType, returnType, + return template.doFind(template.createDelegate(query), getCollectionName(), queryObject, fieldsObject, domainType, + returnType, getCursorPreparer(query, preparer)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 3738a48df0..2afbcd0fe2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -55,6 +55,7 @@ import org.springframework.data.mongodb.MongoDatabaseUtils; import org.springframework.data.mongodb.SessionSynchronization; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; +import org.springframework.data.mongodb.core.CollectionPreparerSupport.CollectionPreparerDelegate; import org.springframework.data.mongodb.core.DefaultBulkOperations.BulkOperationContext; import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity; import org.springframework.data.mongodb.core.QueryOperations.AggregationDefinition; @@ -66,6 +67,7 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; +import org.springframework.data.mongodb.core.aggregation.AggregationOptions.Builder; import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; @@ -83,7 +85,6 @@ import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Meta; -import org.springframework.data.mongodb.core.query.Meta.CursorOption; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; @@ -112,7 +113,23 @@ import com.mongodb.client.result.UpdateResult; /** - * Primary implementation of {@link MongoOperations}. + * Primary implementation of {@link MongoOperations}. It simplifies the use of imperative MongoDB usage and helps to + * avoid common errors. It executes core MongoDB workflow, leaving application code to provide {@link Document} and + * extract results. This class executes BSON queries or updates, initiating iteration over {@link FindIterable} and + * catching MongoDB exceptions and translating them to the generic, more informative exception hierarchy defined in the + * org.springframework.dao package. Can be used within a service implementation via direct instantiation with a + * {@link MongoDatabaseFactory} reference, or get prepared in an application context and given to services as bean + * reference. + *

+ * Note: The {@link MongoDatabaseFactory} should always be configured as a bean in the application context, in the first + * case given to the service directly, in the second case to the prepared template. + *

{@link ReadPreference} and {@link com.mongodb.ReadConcern}

+ *

+ * {@code ReadPreference} and {@code ReadConcern} are generally considered from {@link Query} and + * {@link AggregationOptions} objects for the action to be executed on a particular {@link MongoCollection}. + *

+ * You can also set the default {@link #setReadPreference(ReadPreference) ReadPreference} on the template level to + * generally apply a {@link ReadPreference}. * * @author Thomas Risberg * @author Graeme Rocher @@ -141,7 +158,8 @@ * @author Bartłomiej Mazur * @author Michael Krog */ -public class MongoTemplate implements MongoOperations, ApplicationContextAware, IndexOperationsProvider { +public class MongoTemplate + implements MongoOperations, ApplicationContextAware, IndexOperationsProvider, ReadPreferenceAware { private static final Log LOGGER = LogFactory.getLog(MongoTemplate.class); private static final WriteResultChecking DEFAULT_WRITE_RESULT_CHECKING = WriteResultChecking.NONE; @@ -293,6 +311,16 @@ public void setReadPreference(@Nullable ReadPreference readPreference) { this.readPreference = readPreference; } + @Override + public boolean hasReadPreference() { + return this.readPreference != null; + } + + @Override + public ReadPreference getReadPreference() { + return this.readPreference; + } + /** * Configure whether lifecycle events such as {@link AfterLoadEvent}, {@link BeforeSaveEvent}, etc. should be * published or whether emission should be suppressed. Enabled by default. @@ -363,10 +391,10 @@ private void useEstimatedCount(boolean enabled, BiPredicate { + this.countExecution = (collectionPreparer, collectionName, filter, options) -> { if (!estimationFilter.test(filter, options)) { - return doExactCount(collectionName, filter, options); + return doExactCount(collectionPreparer, collectionName, filter, options); } EstimatedDocumentCountOptions estimatedDocumentCountOptions = new EstimatedDocumentCountOptions(); @@ -374,7 +402,7 @@ private void useEstimatedCount(boolean enabled, BiPredicate Stream doStream(Query query, Class entityType, String collec Document mappedQuery = queryContext.getMappedQuery(persistentEntity); Document mappedFields = queryContext.getMappedFields(persistentEntity, projection); + CollectionPreparerDelegate readPreference = createDelegate(query); FindIterable cursor = new QueryCursorPreparer(query, entityType).initiateFind(collection, - col -> col.find(mappedQuery, Document.class).projection(mappedFields)); + col -> readPreference.prepare(col).find(mappedQuery, Document.class).projection(mappedFields)); return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator, new ProjectingReadCallback<>(mongoConverter, projection, collectionName)).stream(); @@ -517,7 +546,7 @@ protected void executeQuery(Query query, String collectionName, DocumentCallback serializeToJsonSafely(queryObject), sortObject, fieldsObject, collectionName)); } - this.executeQueryInternal(new FindCallback(queryObject, fieldsObject, null), + this.executeQueryInternal(new FindCallback(createDelegate(query), queryObject, fieldsObject, null), preparer != null ? preparer : CursorPreparer.NO_OP_PREPARER, documentCallbackHandler, collectionName); } @@ -765,7 +794,7 @@ public T findOne(Query query, Class entityClass, String collectionName) { if (ObjectUtils.isEmpty(query.getSortObject())) { - return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), + return doFindOne(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(), new QueryCursorPreparer(query, entityClass), entityClass); } else { query.limit(1); @@ -797,7 +826,7 @@ public boolean exists(Query query, @Nullable Class entityClass, String collec Document mappedQuery = queryContext.getMappedQuery(entityClass, this::getPersistentEntity); return execute(collectionName, - new ExistsCallback(mappedQuery, queryContext.getCollation(entityClass).orElse(null))); + new ExistsCallback(createDelegate(query), mappedQuery, queryContext.getCollation(entityClass).orElse(null))); } // Find methods that take a Query to express the query and that return a List of objects. @@ -814,7 +843,7 @@ public List find(Query query, Class entityClass, String collectionName Assert.notNull(collectionName, "CollectionName must not be null"); Assert.notNull(entityClass, "EntityClass must not be null"); - return doFind(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass, + return doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(), entityClass, new QueryCursorPreparer(query, entityClass)); } @@ -834,7 +863,8 @@ public T findById(Object id, Class entityClass, String collectionName) { String idKey = operations.getIdPropertyName(entityClass); - return doFindOne(collectionName, new Document(idKey, id), new Document(), entityClass); + return doFindOne(collectionName, CollectionPreparer.identity(), new Document(idKey, id), new Document(), + entityClass); } @Override @@ -867,10 +897,7 @@ public List findDistinct(Query query, String field, String collectionName serializeToJsonSafely(mappedQuery), field, collectionName)); } - QueryCursorPreparer preparer = new QueryCursorPreparer(query, entityClass); - if (preparer.hasReadPreference()) { - collection = collection.withReadPreference(preparer.getReadPreference()); - } + collection = createDelegate(query).prepare(collection); DistinctIterable iterable = collection.distinct(mappedFieldName, mappedQuery, mongoDriverCompatibleType); distinctQueryContext.applyCollation(entityClass, iterable::collation); @@ -920,8 +947,15 @@ public GeoResults geoNear(NearQuery near, Class domainType, String col String collection = StringUtils.hasText(collectionName) ? collectionName : getCollectionName(domainType); String distanceField = operations.nearQueryDistanceFieldName(domainType); + Builder optionsBuilder = AggregationOptions.builder().collation(near.getCollation()); + Query query = near.getQuery(); + + if (query != null && query.hasReadPreference()) { + optionsBuilder.readPreference(query.getReadPreference()); + } + Aggregation $geoNear = TypedAggregation.newAggregation(domainType, Aggregation.geoNear(near, distanceField)) - .withOptions(AggregationOptions.builder().collation(near.getCollation()).build()); + .withOptions(optionsBuilder.build()); AggregationResults results = aggregate($geoNear, collection, Document.class); EntityProjection projection = operations.introspectProjection(returnType, domainType); @@ -986,7 +1020,7 @@ public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOp operations.forType(entityClass).getCollation(query).ifPresent(optionsToUse::collation); } - return doFindAndModify(collectionName, query.getQueryObject(), query.getFieldsObject(), + return doFindAndModify(createDelegate(query), collectionName, query.getQueryObject(), query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); } @@ -1008,6 +1042,7 @@ public T findAndReplace(Query query, S replacement, FindAndReplaceOptions QueryContext queryContext = queryOperations.createQueryContext(query); EntityProjection projection = operations.introspectProjection(resultType, entityType); + CollectionPreparerDelegate collectionPreparer = createDelegate(query); Document mappedQuery = queryContext.getMappedQuery(entity); Document mappedFields = queryContext.getMappedFields(entity, projection); Document mappedSort = queryContext.getMappedSort(entity); @@ -1018,7 +1053,7 @@ public T findAndReplace(Query query, S replacement, FindAndReplaceOptions maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName)); maybeCallBeforeSave(replacement, mappedReplacement, collectionName); - T saved = doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, + T saved = doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, projection); if (saved != null) { @@ -1046,7 +1081,7 @@ public T findAndRemove(Query query, Class entityClass, String collectionN Assert.notNull(entityClass, "EntityClass must not be null"); Assert.notNull(collectionName, "CollectionName must not be null"); - return doFindAndRemove(collectionName, query.getQueryObject(), query.getFieldsObject(), + return doFindAndRemove(createDelegate(query), collectionName, query.getQueryObject(), query.getFieldsObject(), getMappedSortObject(query, entityClass), operations.forType(entityClass).getCollation(query).orElse(null), entityClass); } @@ -1078,17 +1113,19 @@ public long count(Query query, @Nullable Class entityClass, String collection CountOptions options = countContext.getCountOptions(entityClass); Document mappedQuery = countContext.getMappedQuery(entityClass, mappingContext::getPersistentEntity); - return doCount(collectionName, mappedQuery, options); + CollectionPreparerDelegate readPreference = createDelegate(query); + return doCount(readPreference, collectionName, mappedQuery, options); } - protected long doCount(String collectionName, Document filter, CountOptions options) { + protected long doCount(CollectionPreparer collectionPreparer, String collectionName, Document filter, + CountOptions options) { if (LOGGER.isDebugEnabled()) { LOGGER .debug(String.format("Executing count: %s in collection: %s", serializeToJsonSafely(filter), collectionName)); } - return countExecution.countDocuments(collectionName, filter, options); + return countExecution.countDocuments(collectionPreparer, collectionName, filter, options); } /* @@ -1097,11 +1134,13 @@ protected long doCount(String collectionName, Document filter, CountOptions opti */ @Override public long estimatedCount(String collectionName) { - return doEstimatedCount(collectionName, new EstimatedDocumentCountOptions()); + return doEstimatedCount(CollectionPreparerDelegate.of(this), collectionName, new EstimatedDocumentCountOptions()); } - protected long doEstimatedCount(String collectionName, EstimatedDocumentCountOptions options) { - return execute(collectionName, collection -> collection.estimatedDocumentCount(options)); + protected long doEstimatedCount(CollectionPreparer> collectionPreparer, + String collectionName, EstimatedDocumentCountOptions options) { + return execute(collectionName, + collection -> collectionPreparer.prepare(collection).estimatedDocumentCount(options)); } @Override @@ -1112,12 +1151,13 @@ public long exactCount(Query query, @Nullable Class entityClass, String colle CountOptions options = countContext.getCountOptions(entityClass); Document mappedQuery = countContext.getMappedQuery(entityClass, mappingContext::getPersistentEntity); - return doExactCount(collectionName, mappedQuery, options); + return doExactCount(createDelegate(query), collectionName, mappedQuery, options); } - protected long doExactCount(String collectionName, Document filter, CountOptions options) { - return execute(collectionName, - collection -> collection.countDocuments(CountQuery.of(filter).toQueryDocument(), options)); + protected long doExactCount(CollectionPreparer> collectionPreparer, String collectionName, + Document filter, CountOptions options) { + return execute(collectionName, collection -> collectionPreparer.prepare(collection) + .countDocuments(CountQuery.of(filter).toQueryDocument(), options)); } protected boolean countCanBeEstimated(Document filter, CountOptions options) { @@ -1177,8 +1217,8 @@ protected void ensureNotCollectionLike(@Nullable Object source) { */ protected MongoCollection prepareCollection(MongoCollection collection) { - if (this.readPreference != null) { - collection = collection.withReadPreference(readPreference); + if (this.readPreference != null && this.readPreference != collection.getReadPreference()) { + return collection.withReadPreference(readPreference); } return collection; @@ -1754,7 +1794,7 @@ public List findAll(Class entityClass) { @Override public List findAll(Class entityClass, String collectionName) { return executeFindMultiInternal( - new FindCallback(new Document(), new Document(), + new FindCallback(CollectionPreparer.identity(), new Document(), new Document(), operations.forType(entityClass).getCollation().map(Collation::toMongoCollation).orElse(null)), CursorPreparer.NO_OP_PREPARER, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), collectionName); @@ -1812,7 +1852,9 @@ public List mapReduce(Query query, Class domainType, String inputColle String mapFunc = replaceWithResourceIfNecessary(mapFunction); String reduceFunc = replaceWithResourceIfNecessary(reduceFunction); - MongoCollection inputCollection = getAndPrepareCollection(doGetDatabase(), inputCollectionName); + CollectionPreparerDelegate readPreference = createDelegate(query); + MongoCollection inputCollection = readPreference + .prepare(getAndPrepareCollection(doGetDatabase(), inputCollectionName)); // MapReduceOp MapReduceIterable mapReduce = inputCollection.mapReduce(mapFunc, reduceFunc, Document.class); @@ -1977,6 +2019,9 @@ protected List doFindAndDelete(String collectionName, Query query, Class< if (!CollectionUtils.isEmpty(result)) { Query byIdInQuery = operations.getByIdInQuery(result); + if (query.hasReadPreference()) { + byIdInQuery.withReadPreference(query.getReadPreference()); + } remove(byIdInQuery, entityClass, collectionName); } @@ -2032,7 +2077,7 @@ protected AggregationResults doAggregate(Aggregation aggregation, String return execute(collectionName, collection -> { List rawResult = new ArrayList<>(); - + CollectionPreparerDelegate delegate = CollectionPreparerDelegate.of(options); Class domainType = aggregation instanceof TypedAggregation ? ((TypedAggregation) aggregation).getInputType() : null; @@ -2040,7 +2085,7 @@ protected AggregationResults doAggregate(Aggregation aggregation, String () -> operations.forType(domainType) // .getCollation()); - AggregateIterable aggregateIterable = collection.aggregate(pipeline, Document.class) // + AggregateIterable aggregateIterable = delegate.prepare(collection).aggregate(pipeline, Document.class) // .collation(collation.map(Collation::toMongoCollation).orElse(null)) // .allowDiskUse(options.isAllowDiskUse()); @@ -2103,7 +2148,9 @@ protected Stream aggregateStream(Aggregation aggregation, String collecti return execute(collectionName, (CollectionCallback>) collection -> { - AggregateIterable cursor = collection.aggregate(pipeline, Document.class) // + CollectionPreparerDelegate delegate = CollectionPreparerDelegate.of(options); + + AggregateIterable cursor = delegate.prepare(collection).aggregate(pipeline, Document.class) // .allowDiskUse(options.isAllowDiskUse()); if (options.getCursorBatchSize() != null) { @@ -2344,14 +2391,16 @@ private CreateCollectionOptions getCreateCollectionOptions(Document document) { * The query document is specified as a standard {@link Document} and so is the fields specification. * * @param collectionName name of the collection to retrieve the objects from. + * @param collectionPreparer the preparer to prepare the collection for the actual use. * @param query the query document that specifies the criteria used to find a record. * @param fields the document that specifies the fields to be returned. * @param entityClass the parameterized type of the returned list. * @return the converted object or {@literal null} if none exists. */ @Nullable - protected T doFindOne(String collectionName, Document query, Document fields, Class entityClass) { - return doFindOne(collectionName, query, fields, CursorPreparer.NO_OP_PREPARER, entityClass); + protected T doFindOne(String collectionName, CollectionPreparer> collectionPreparer, + Document query, Document fields, Class entityClass) { + return doFindOne(collectionName, collectionPreparer, query, fields, CursorPreparer.NO_OP_PREPARER, entityClass); } /** @@ -2359,17 +2408,18 @@ protected T doFindOne(String collectionName, Document query, Document fields * The query document is specified as a standard {@link Document} and so is the fields specification. * * @param collectionName name of the collection to retrieve the objects from. + * @param collectionPreparer the preparer to prepare the collection for the actual use. * @param query the query document that specifies the criteria used to find a record. * @param fields the document that specifies the fields to be returned. - * @param entityClass the parameterized type of the returned list. * @param preparer the preparer used to modify the cursor on execution. + * @param entityClass the parameterized type of the returned list. * @return the converted object or {@literal null} if none exists. * @since 2.2 */ @Nullable @SuppressWarnings("ConstantConditions") - protected T doFindOne(String collectionName, Document query, Document fields, CursorPreparer preparer, - Class entityClass) { + protected T doFindOne(String collectionName, CollectionPreparer> collectionPreparer, + Document query, Document fields, CursorPreparer preparer, Class entityClass) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); @@ -2382,7 +2432,7 @@ protected T doFindOne(String collectionName, Document query, Document fields serializeToJsonSafely(query), mappedFields, entityClass, collectionName)); } - return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields, preparer), + return executeFindOneInternal(new FindOneCallback(collectionPreparer, mappedQuery, mappedFields, preparer), new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); } @@ -2391,13 +2441,15 @@ protected T doFindOne(String collectionName, Document query, Document fields * query document is specified as a standard Document and so is the fields specification. * * @param collectionName name of the collection to retrieve the objects from + * @param collectionPreparer the preparer to prepare the collection for the actual use. * @param query the query document that specifies the criteria used to find a record * @param fields the document that specifies the fields to be returned * @param entityClass the parameterized type of the returned list. * @return the List of converted objects. */ - protected List doFind(String collectionName, Document query, Document fields, Class entityClass) { - return doFind(collectionName, query, fields, entityClass, null, + protected List doFind(String collectionName, CollectionPreparer> collectionPreparer, + Document query, Document fields, Class entityClass) { + return doFind(collectionName, collectionPreparer, query, fields, entityClass, null, new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName)); } @@ -2407,6 +2459,7 @@ protected List doFind(String collectionName, Document query, Document fie * specified as a standard Document and so is the fields specification. * * @param collectionName name of the collection to retrieve the objects from. + * @param collectionPreparer the preparer to prepare the collection for the actual use. * @param query the query document that specifies the criteria used to find a record. * @param fields the document that specifies the fields to be returned. * @param entityClass the parameterized type of the returned list. @@ -2414,14 +2467,15 @@ protected List doFind(String collectionName, Document query, Document fie * (apply limits, skips and so on). * @return the {@link List} of converted objects. */ - protected List doFind(String collectionName, Document query, Document fields, Class entityClass, - CursorPreparer preparer) { - return doFind(collectionName, query, fields, entityClass, preparer, + protected List doFind(String collectionName, CollectionPreparer> collectionPreparer, + Document query, Document fields, Class entityClass, CursorPreparer preparer) { + return doFind(collectionName, collectionPreparer, query, fields, entityClass, preparer, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName)); } - protected List doFind(String collectionName, Document query, Document fields, Class entityClass, - @Nullable CursorPreparer preparer, DocumentCallback objectCallback) { + protected List doFind(String collectionName, + CollectionPreparer> collectionPreparer, Document query, Document fields, + Class entityClass, @Nullable CursorPreparer preparer, DocumentCallback objectCallback) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); @@ -2434,7 +2488,7 @@ protected List doFind(String collectionName, Document query, Document serializeToJsonSafely(mappedQuery), mappedFields, entityClass, collectionName)); } - return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields, null), + return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields, null), preparer != null ? preparer : CursorPreparer.NO_OP_PREPARER, objectCallback, collectionName); } @@ -2444,8 +2498,8 @@ protected List doFind(String collectionName, Document query, Document * * @since 2.0 */ - List doFind(String collectionName, Document query, Document fields, Class sourceClass, - Class targetClass, CursorPreparer preparer) { + List doFind(CollectionPreparer> collectionPreparer, String collectionName, + Document query, Document fields, Class sourceClass, Class targetClass, CursorPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); @@ -2459,7 +2513,7 @@ List doFind(String collectionName, Document query, Document fields, Cl serializeToJsonSafely(mappedQuery), mappedFields, sourceClass, collectionName)); } - return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields, null), preparer, + return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields, null), preparer, new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName); } @@ -2533,8 +2587,8 @@ Document getMappedValidator(Validator validator, Class domainType) { * @return the List of converted objects. */ @SuppressWarnings("ConstantConditions") - protected T doFindAndRemove(String collectionName, Document query, Document fields, Document sort, - @Nullable Collation collation, Class entityClass) { + protected T doFindAndRemove(CollectionPreparer collectionPreparer, String collectionName, Document query, + Document fields, Document sort, @Nullable Collation collation, Class entityClass) { EntityReader readerToUse = this.mongoConverter; @@ -2545,14 +2599,15 @@ protected T doFindAndRemove(String collectionName, Document query, Document MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); - return executeFindOneInternal( - new FindAndRemoveCallback(queryMapper.getMappedObject(query, entity), fields, sort, collation), + return executeFindOneInternal(new FindAndRemoveCallback(collectionPreparer, + queryMapper.getMappedObject(query, entity), fields, sort, collation), new ReadDocumentCallback<>(readerToUse, entityClass, collectionName), collectionName); } @SuppressWarnings("ConstantConditions") - protected T doFindAndModify(String collectionName, Document query, Document fields, Document sort, - Class entityClass, UpdateDefinition update, @Nullable FindAndModifyOptions options) { + protected T doFindAndModify(CollectionPreparer collectionPreparer, String collectionName, Document query, + Document fields, Document sort, Class entityClass, UpdateDefinition update, + @Nullable FindAndModifyOptions options) { EntityReader readerToUse = this.mongoConverter; @@ -2577,7 +2632,7 @@ protected T doFindAndModify(String collectionName, Document query, Document } return executeFindOneInternal( - new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate, + new FindAndModifyCallback(collectionPreparer, mappedQuery, fields, sort, mappedUpdate, update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options), new ReadDocumentCallback<>(readerToUse, entityClass, collectionName), collectionName); } @@ -2598,14 +2653,18 @@ protected T doFindAndModify(String collectionName, Document query, Document * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. */ @Nullable - protected T doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, - Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class entityType, - Document replacement, FindAndReplaceOptions options, Class resultType) { + protected T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery, + Document mappedFields, Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, + Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { EntityProjection projection = operations.introspectProjection(resultType, entityType); - return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement, - options, projection); + return doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, collation, + entityType, replacement, options, projection); + } + + CollectionPreparerDelegate createDelegate(Query query) { + return CollectionPreparerDelegate.of(query); } /** @@ -2625,9 +2684,9 @@ protected T doFindAndReplace(String collectionName, Document mappedQuery, Do * @since 3.4 */ @Nullable - private T doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, - Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class entityType, - Document replacement, FindAndReplaceOptions options, EntityProjection projection) { + private T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery, + Document mappedFields, Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, + Class entityType, Document replacement, FindAndReplaceOptions options, EntityProjection projection) { if (LOGGER.isDebugEnabled()) { LOGGER @@ -2638,9 +2697,9 @@ private T doFindAndReplace(String collectionName, Document mappedQuery, Docu serializeToJsonSafely(mappedSort), entityType, serializeToJsonSafely(replacement), collectionName)); } - return executeFindOneInternal( - new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), - new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName); + return executeFindOneInternal(new FindAndReplaceCallback(collectionPreparer, mappedQuery, mappedFields, mappedSort, + replacement, collation, options), new ProjectingReadCallback<>(mongoConverter, projection, collectionName), + collectionName); } /** @@ -2810,12 +2869,15 @@ static RuntimeException potentiallyConvertRuntimeException(RuntimeException ex, */ private static class FindOneCallback implements CollectionCallback { + private final CollectionPreparer> collectionPreparer; private final Document query; private final Optional fields; private final CursorPreparer cursorPreparer; - FindOneCallback(Document query, Document fields, CursorPreparer preparer) { + FindOneCallback(CollectionPreparer> collectionPreparer, Document query, Document fields, + CursorPreparer preparer) { + this.collectionPreparer = collectionPreparer; this.query = query; this.fields = Optional.of(fields).filter(it -> !ObjectUtils.isEmpty(fields)); this.cursorPreparer = preparer; @@ -2824,7 +2886,8 @@ private static class FindOneCallback implements CollectionCallback { @Override public Document doInCollection(MongoCollection collection) throws MongoException, DataAccessException { - FindIterable iterable = cursorPreparer.initiateFind(collection, col -> col.find(query, Document.class)); + FindIterable iterable = cursorPreparer.initiateFind(collection, + col -> collectionPreparer.prepare(col).find(query, Document.class)); if (LOGGER.isDebugEnabled()) { @@ -2851,15 +2914,18 @@ public Document doInCollection(MongoCollection collection) throws Mong */ private static class FindCallback implements CollectionCallback> { + private final CollectionPreparer> collectionPreparer; private final Document query; private final Document fields; private final @Nullable com.mongodb.client.model.Collation collation; - public FindCallback(Document query, Document fields, @Nullable com.mongodb.client.model.Collation collation) { + public FindCallback(CollectionPreparer> collectionPreparer, Document query, + Document fields, @Nullable com.mongodb.client.model.Collation collation) { Assert.notNull(query, "Query must not be null"); Assert.notNull(fields, "Fields must not be null"); + this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; this.collation = collation; @@ -2869,7 +2935,8 @@ public FindCallback(Document query, Document fields, @Nullable com.mongodb.clien public FindIterable doInCollection(MongoCollection collection) throws MongoException, DataAccessException { - FindIterable findIterable = collection.find(query, Document.class).projection(fields); + FindIterable findIterable = collectionPreparer.prepare(collection).find(query, Document.class) + .projection(fields); if (collation != null) { findIterable = findIterable.collation(collation); @@ -2887,11 +2954,14 @@ public FindIterable doInCollection(MongoCollection collectio */ private class ExistsCallback implements CollectionCallback { + private final CollectionPreparer collectionPreparer; private final Document mappedQuery; private final com.mongodb.client.model.Collation collation; - ExistsCallback(Document mappedQuery, com.mongodb.client.model.Collation collation) { + ExistsCallback(CollectionPreparer collectionPreparer, Document mappedQuery, + com.mongodb.client.model.Collation collation) { + this.collectionPreparer = collectionPreparer; this.mappedQuery = mappedQuery; this.collation = collation; } @@ -2899,7 +2969,7 @@ private class ExistsCallback implements CollectionCallback { @Override public Boolean doInCollection(MongoCollection collection) throws MongoException, DataAccessException { - return doCount(collection.getNamespace().getCollectionName(), mappedQuery, + return doCount(collectionPreparer, collection.getNamespace().getCollectionName(), mappedQuery, new CountOptions().limit(1).collation(collation)) > 0; } } @@ -2912,12 +2982,15 @@ public Boolean doInCollection(MongoCollection collection) throws Mongo */ private static class FindAndRemoveCallback implements CollectionCallback { + private final CollectionPreparer> collectionPreparer; private final Document query; private final Document fields; private final Document sort; private final Optional collation; - FindAndRemoveCallback(Document query, Document fields, Document sort, @Nullable Collation collation) { + FindAndRemoveCallback(CollectionPreparer> collectionPreparer, Document query, + Document fields, Document sort, @Nullable Collation collation) { + this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; @@ -2931,12 +3004,13 @@ public Document doInCollection(MongoCollection collection) throws Mong FindOneAndDeleteOptions opts = new FindOneAndDeleteOptions().sort(sort).projection(fields); collation.map(Collation::toMongoCollation).ifPresent(opts::collation); - return collection.findOneAndDelete(query, opts); + return collectionPreparer.prepare(collection).findOneAndDelete(query, opts); } } private static class FindAndModifyCallback implements CollectionCallback { + private final CollectionPreparer> collectionPreparer; private final Document query; private final Document fields; private final Document sort; @@ -2944,9 +3018,10 @@ private static class FindAndModifyCallback implements CollectionCallback arrayFilters; private final FindAndModifyOptions options; - FindAndModifyCallback(Document query, Document fields, Document sort, Object update, List arrayFilters, - FindAndModifyOptions options) { + FindAndModifyCallback(CollectionPreparer> collectionPreparer, Document query, + Document fields, Document sort, Object update, List arrayFilters, FindAndModifyOptions options) { + this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; this.sort = sort; @@ -2975,9 +3050,9 @@ public Document doInCollection(MongoCollection collection) throws Mong } if (update instanceof Document) { - return collection.findOneAndUpdate(query, (Document) update, opts); + return collectionPreparer.prepare(collection).findOneAndUpdate(query, (Document) update, opts); } else if (update instanceof List) { - return collection.findOneAndUpdate(query, (List) update, opts); + return collectionPreparer.prepare(collection).findOneAndUpdate(query, (List) update, opts); } throw new IllegalArgumentException(String.format("Using %s is not supported in findOneAndUpdate", update)); @@ -2993,6 +3068,7 @@ public Document doInCollection(MongoCollection collection) throws Mong */ private static class FindAndReplaceCallback implements CollectionCallback { + private final CollectionPreparer> collectionPreparer; private final Document query; private final Document fields; private final Document sort; @@ -3000,9 +3076,10 @@ private static class FindAndReplaceCallback implements CollectionCallback> collectionPreparer, Document query, + Document fields, Document sort, Document update, @Nullable com.mongodb.client.model.Collation collation, + FindAndReplaceOptions options) { + this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; this.sort = sort; @@ -3027,7 +3104,7 @@ public Document doInCollection(MongoCollection collection) throws Mong opts.returnDocument(ReturnDocument.AFTER); } - return collection.findOneAndReplace(query, update, opts); + return collectionPreparer.prepare(collection).findOneAndReplace(query, update, opts); } } @@ -3209,11 +3286,6 @@ public FindIterable prepare(FindIterable iterable) { return cursorToUse; } - @Override - public ReadPreference getReadPreference() { - return query.getMeta().getFlags().contains(CursorOption.SECONDARY_READS) ? ReadPreference.primaryPreferred() - : null; - } } /** @@ -3399,6 +3471,7 @@ protected boolean countCanBeEstimated(Document filter, CountOptions options) { @FunctionalInterface interface CountExecution { - long countDocuments(String collection, Document filter, CountOptions options); + long countDocuments(CollectionPreparer collectionPreparer, String collection, Document filter, + CountOptions options); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java index 46d27b68bb..4dcf62aacd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java @@ -20,6 +20,7 @@ import org.bson.Document; import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.mongodb.core.CollectionPreparerSupport.ReactiveCollectionPreparerDelegate; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.SerializationUtils; @@ -67,8 +68,8 @@ static class ReactiveFindSupport private final String collection; private final Query query; - ReactiveFindSupport(ReactiveMongoTemplate template, Class domainType, Class returnType, - String collection, Query query) { + ReactiveFindSupport(ReactiveMongoTemplate template, Class domainType, Class returnType, String collection, + Query query) { this.template = template; this.domainType = domainType; @@ -169,8 +170,8 @@ private Flux doFind(@Nullable FindPublisherPreparer preparer) { Document queryObject = query.getQueryObject(); Document fieldsObject = query.getFieldsObject(); - return template.doFind(getCollectionName(), queryObject, fieldsObject, domainType, returnType, - preparer != null ? preparer : getCursorPreparer(query)); + return template.doFind(getCollectionName(), ReactiveCollectionPreparerDelegate.of(query), queryObject, + fieldsObject, domainType, returnType, preparer != null ? preparer : getCursorPreparer(query)); } @SuppressWarnings("unchecked") diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 5e33adb753..1eb27c652e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -70,6 +70,7 @@ import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; import org.springframework.data.mongodb.ReactiveMongoDatabaseUtils; import org.springframework.data.mongodb.SessionSynchronization; +import org.springframework.data.mongodb.core.CollectionPreparerSupport.ReactiveCollectionPreparerDelegate; import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity; import org.springframework.data.mongodb.core.QueryOperations.AggregationDefinition; import org.springframework.data.mongodb.core.QueryOperations.CountContext; @@ -105,7 +106,6 @@ import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Meta; -import org.springframework.data.mongodb.core.query.Meta.CursorOption; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; @@ -147,9 +147,18 @@ * extract results. This class executes BSON queries or updates, initiating iteration over {@link FindPublisher} and * catching MongoDB exceptions and translating them to the generic, more informative exception hierarchy defined in the * org.springframework.dao package. Can be used within a service implementation via direct instantiation with a - * {@link SimpleReactiveMongoDatabaseFactory} reference, or get prepared in an application context and given to services - * as bean reference. Note: The {@link SimpleReactiveMongoDatabaseFactory} should always be configured as a bean in the - * application context, in the first case given to the service directly, in the second case to the prepared template. + * {@link ReactiveMongoDatabaseFactory} reference, or get prepared in an application context and given to services as + * bean reference. + *

+ * Note: The {@link ReactiveMongoDatabaseFactory} should always be configured as a bean in the application context, in + * the first case given to the service directly, in the second case to the prepared template. + *

{@link ReadPreference} and {@link com.mongodb.ReadConcern}

+ *

+ * {@code ReadPreference} and {@code ReadConcern} are generally considered from {@link Query} and + * {@link AggregationOptions} objects for the action to be executed on a particular {@link MongoCollection}. + *

+ * You can also set the default {@link #setReadPreference(ReadPreference) ReadPreference} on the template level to + * generally apply a {@link ReadPreference}. * * @author Mark Paluch * @author Christoph Strobl @@ -756,8 +765,8 @@ public Mono findOne(Query query, Class entityClass) { public Mono findOne(Query query, Class entityClass, String collectionName) { if (ObjectUtils.isEmpty(query.getSortObject())) { - return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass, - new QueryFindPublisherPreparer(query, entityClass)); + return doFindOne(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), + query.getFieldsObject(), entityClass, new QueryFindPublisherPreparer(query, entityClass)); } query.limit(1); @@ -783,10 +792,11 @@ public Mono exists(Query query, @Nullable Class entityClass, String return createFlux(collectionName, collection -> { + ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(query); QueryContext queryContext = queryOperations.createQueryContext(query); Document filter = queryContext.getMappedQuery(entityClass, this::getPersistentEntity); - FindPublisher findPublisher = collection.find(filter, Document.class) + FindPublisher findPublisher = collectionPreparer.prepare(collection).find(filter, Document.class) .projection(new Document("_id", 1)); if (LOGGER.isDebugEnabled()) { @@ -811,8 +821,8 @@ public Flux find(@Nullable Query query, Class entityClass, String coll return findAll(entityClass, collectionName); } - return doFind(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass, - new QueryFindPublisherPreparer(query, entityClass)); + return doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), + query.getFieldsObject(), entityClass, new QueryFindPublisherPreparer(query, entityClass)); } @Override @@ -825,7 +835,8 @@ public Mono findById(Object id, Class entityClass, String collectionNa String idKey = operations.getIdPropertyName(entityClass); - return doFindOne(collectionName, new Document(idKey, id), null, entityClass, (Collation) null); + return doFindOne(collectionName, CollectionPreparer.identity(), new Document(idKey, id), null, entityClass, + (Collation) null); } @Override @@ -850,6 +861,7 @@ public Flux findDistinct(Query query, String field, String collectionName Document mappedQuery = distinctQueryContext.getMappedQuery(entity); String mappedFieldName = distinctQueryContext.getMappedFieldName(entity); Class mongoDriverCompatibleType = distinctQueryContext.getDriverCompatibleClass(resultClass); + ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(query); Flux result = execute(collectionName, collection -> { @@ -859,11 +871,9 @@ public Flux findDistinct(Query query, String field, String collectionName } FindPublisherPreparer preparer = new QueryFindPublisherPreparer(query, entityClass); - if (preparer.hasReadPreference()) { - collection = collection.withReadPreference(preparer.getReadPreference()); - } - DistinctPublisher publisher = collection.distinct(mappedFieldName, mappedQuery, mongoDriverCompatibleType); + DistinctPublisher publisher = collectionPreparer.prepare(collection).distinct(mappedFieldName, mappedQuery, + mongoDriverCompatibleType); distinctQueryContext.applyCollation(entityClass, publisher::collation); return publisher; }); @@ -929,7 +939,8 @@ private Flux aggregateAndMap(MongoCollection collection, List readCallback, @Nullable Class inputType) { - AggregatePublisher cursor = collection.aggregate(pipeline, Document.class) + ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(options); + AggregatePublisher cursor = collectionPreparer.prepare(collection).aggregate(pipeline, Document.class) .allowDiskUse(options.isAllowDiskUse()); if (options.getCursorBatchSize() != null) { @@ -1028,8 +1039,8 @@ public Mono findAndModify(Query query, UpdateDefinition update, FindAndMo operations.forType(entityClass).getCollation(query).ifPresent(optionsToUse::collation); } - return doFindAndModify(collectionName, query.getQueryObject(), query.getFieldsObject(), - getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); + return doFindAndModify(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), + query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); } @Override @@ -1053,6 +1064,7 @@ public Mono findAndReplace(Query query, S replacement, FindAndReplaceO Document mappedQuery = queryContext.getMappedQuery(entity); Document mappedFields = queryContext.getMappedFields(entity, projection); Document mappedSort = queryContext.getMappedSort(entity); + ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(query); return Mono.defer(() -> { @@ -1070,8 +1082,9 @@ public Mono findAndReplace(Query query, S replacement, FindAndReplaceO mapped.getCollection())); }).flatMap(it -> { - Mono afterFindAndReplace = doFindAndReplace(it.getCollection(), mappedQuery, mappedFields, mappedSort, - queryContext.getCollation(entityType).orElse(null), entityType, it.getTarget(), options, projection); + Mono afterFindAndReplace = doFindAndReplace(it.getCollection(), collectionPreparer, mappedQuery, + mappedFields, mappedSort, queryContext.getCollation(entityType).orElse(null), entityType, it.getTarget(), + options, projection); return afterFindAndReplace.flatMap(saved -> { maybeEmitEvent(new AfterSaveEvent<>(saved, it.getTarget(), it.getCollection())); return maybeCallAfterSave(saved, it.getTarget(), it.getCollection()); @@ -1089,9 +1102,9 @@ public Mono findAndRemove(Query query, Class entityClass) { public Mono findAndRemove(Query query, Class entityClass, String collectionName) { operations.forType(entityClass).getCollation(query); - return doFindAndRemove(collectionName, query.getQueryObject(), query.getFieldsObject(), - getMappedSortObject(query, entityClass), operations.forType(entityClass).getCollation(query).orElse(null), - entityClass); + return doFindAndRemove(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), + query.getFieldsObject(), getMappedSortObject(query, entityClass), + operations.forType(entityClass).getCollation(query).orElse(null), entityClass); } /* @@ -1799,12 +1812,14 @@ protected Mono doRemove(String collectionName, Query query, @N MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.REMOVE, collectionName, entityClass, null, removeQuery); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); + ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(query); return execute(collectionName, collection -> { maybeEmitEvent(new BeforeDeleteEvent<>(removeQuery, entityClass, collectionName)); - MongoCollection collectionToUse = prepareCollection(collection, writeConcernToUse); + MongoCollection collectionToUse = collectionPreparer + .prepare(prepareCollection(collection, writeConcernToUse)); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("Remove using query: %s in collection: %s.", serializeToJsonSafely(removeQuery), @@ -1839,8 +1854,9 @@ public Flux findAll(Class entityClass) { @Override public Flux findAll(Class entityClass, String collectionName) { - return executeFindMultiInternal(new FindCallback(null), FindPublisherPreparer.NO_OP_PREPARER, - new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), collectionName); + return executeFindMultiInternal(new FindCallback(CollectionPreparer.identity(), null), + FindPublisherPreparer.NO_OP_PREPARER, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), + collectionName); } @Override @@ -1867,17 +1883,19 @@ public Flux tail(Query query, Class entityClass) { @Override public Flux tail(@Nullable Query query, Class entityClass, String collectionName) { + ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(query); if (query == null) { LOGGER.debug(String.format("Tail for class: %s in collection: %s", entityClass, collectionName)); return executeFindMultiInternal( - collection -> new FindCallback(null).doInCollection(collection).cursorType(CursorType.TailableAwait), + collection -> new FindCallback(collectionPreparer, null).doInCollection(collection) + .cursorType(CursorType.TailableAwait), FindPublisherPreparer.NO_OP_PREPARER, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), collectionName); } - return doFind(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass, + return doFind(collectionName, collectionPreparer, query.getQueryObject(), query.getFieldsObject(), entityClass, new TailingQueryFindPublisherPreparer(query, entityClass)); } @@ -1961,12 +1979,14 @@ public Flux mapReduce(Query filterQuery, Class domainType, String inpu assertLocalFunctionNames(mapFunction, reduceFunction); + ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(filterQuery); return createFlux(inputCollectionName, collection -> { Document mappedQuery = queryMapper.getMappedObject(filterQuery.getQueryObject(), mappingContext.getPersistentEntity(domainType)); - MapReducePublisher publisher = collection.mapReduce(mapFunction, reduceFunction, Document.class); + MapReducePublisher publisher = collectionPreparer.prepare(collection).mapReduce(mapFunction, + reduceFunction, Document.class); publisher.filter(mappedQuery); @@ -2133,16 +2153,18 @@ protected Mono> doCreateCollection(String collectionNa * The query document is specified as a standard {@link Document} and so is the fields specification. * * @param collectionName name of the collection to retrieve the objects from. + * @param collectionPreparer the preparer to prepare the collection for the actual use. * @param query the query document that specifies the criteria used to find a record. * @param fields the document that specifies the fields to be returned. * @param entityClass the parameterized type of the returned list. * @param collation can be {@literal null}. * @return the {@link List} of converted objects. */ - protected Mono doFindOne(String collectionName, Document query, @Nullable Document fields, + protected Mono doFindOne(String collectionName, + CollectionPreparer> collectionPreparer, Document query, @Nullable Document fields, Class entityClass, @Nullable Collation collation) { - return doFindOne(collectionName, query, fields, entityClass, + return doFindOne(collectionName, collectionPreparer, query, fields, entityClass, findPublisher -> collation != null ? findPublisher.collation(collation.toMongoCollation()) : findPublisher); } @@ -2151,6 +2173,7 @@ protected Mono doFindOne(String collectionName, Document query, @Nullable * The query document is specified as a standard {@link Document} and so is the fields specification. * * @param collectionName name of the collection to retrieve the objects from. + * @param collectionPreparer the preparer to prepare the collection for the actual use. * @param query the query document that specifies the criteria used to find a record. * @param fields the document that specifies the fields to be returned. * @param entityClass the parameterized type of the returned list. @@ -2158,7 +2181,8 @@ protected Mono doFindOne(String collectionName, Document query, @Nullable * @return the {@link List} of converted objects. * @since 2.2 */ - protected Mono doFindOne(String collectionName, Document query, @Nullable Document fields, + protected Mono doFindOne(String collectionName, + CollectionPreparer> collectionPreparer, Document query, @Nullable Document fields, Class entityClass, FindPublisherPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); @@ -2173,7 +2197,7 @@ protected Mono doFindOne(String collectionName, Document query, @Nullable serializeToJsonSafely(query), mappedFields, entityClass, collectionName)); } - return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields, preparer), + return executeFindOneInternal(new FindOneCallback(collectionPreparer, mappedQuery, mappedFields, preparer), new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); } @@ -2182,13 +2206,15 @@ protected Mono doFindOne(String collectionName, Document query, @Nullable * query document is specified as a standard Document and so is the fields specification. * * @param collectionName name of the collection to retrieve the objects from + * @param collectionPreparer the preparer to prepare the collection for the actual use. * @param query the query document that specifies the criteria used to find a record * @param fields the document that specifies the fields to be returned * @param entityClass the parameterized type of the returned list. * @return the List of converted objects. */ - protected Flux doFind(String collectionName, Document query, Document fields, Class entityClass) { - return doFind(collectionName, query, fields, entityClass, null, + protected Flux doFind(String collectionName, CollectionPreparer> collectionPreparer, + Document query, Document fields, Class entityClass) { + return doFind(collectionName, collectionPreparer, query, fields, entityClass, null, new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName)); } @@ -2198,6 +2224,7 @@ protected Flux doFind(String collectionName, Document query, Document fie * specified as a standard Document and so is the fields specification. * * @param collectionName name of the collection to retrieve the objects from. + * @param collectionPreparer the preparer to prepare the collection for the actual use. * @param query the query document that specifies the criteria used to find a record. * @param fields the document that specifies the fields to be returned. * @param entityClass the parameterized type of the returned list. @@ -2205,14 +2232,15 @@ protected Flux doFind(String collectionName, Document query, Document fie * the result set, (apply limits, skips and so on). * @return the {@link List} of converted objects. */ - protected Flux doFind(String collectionName, Document query, Document fields, Class entityClass, - FindPublisherPreparer preparer) { - return doFind(collectionName, query, fields, entityClass, preparer, + protected Flux doFind(String collectionName, CollectionPreparer> collectionPreparer, + Document query, Document fields, Class entityClass, FindPublisherPreparer preparer) { + return doFind(collectionName, collectionPreparer, query, fields, entityClass, preparer, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName)); } - protected Flux doFind(String collectionName, Document query, Document fields, Class entityClass, - @Nullable FindPublisherPreparer preparer, DocumentCallback objectCallback) { + protected Flux doFind(String collectionName, + CollectionPreparer> collectionPreparer, Document query, Document fields, + Class entityClass, @Nullable FindPublisherPreparer preparer, DocumentCallback objectCallback) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); @@ -2225,8 +2253,8 @@ protected Flux doFind(String collectionName, Document query, Document serializeToJsonSafely(mappedQuery), mappedFields, entityClass, collectionName)); } - return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields), preparer, objectCallback, - collectionName); + return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields), preparer, + objectCallback, collectionName); } /** @@ -2235,8 +2263,8 @@ protected Flux doFind(String collectionName, Document query, Document * * @since 2.0 */ - Flux doFind(String collectionName, Document query, Document fields, Class sourceClass, - Class targetClass, FindPublisherPreparer preparer) { + Flux doFind(String collectionName, CollectionPreparer> collectionPreparer, + Document query, Document fields, Class sourceClass, Class targetClass, FindPublisherPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); @@ -2250,7 +2278,7 @@ Flux doFind(String collectionName, Document query, Document fields, Cl serializeToJsonSafely(mappedQuery), mappedFields, sourceClass, collectionName)); } - return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields), preparer, + return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields), preparer, new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName); } @@ -2268,13 +2296,15 @@ protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Col * The first document that matches the query is returned and also removed from the collection in the database.
* The query document is specified as a standard Document and so is the fields specification. * - * @param collectionName name of the collection to retrieve the objects from - * @param query the query document that specifies the criteria used to find a record - * @param collation collation + * @param collectionName name of the collection to retrieve the objects from. + * @param collectionPreparer the preparer to prepare the collection for the actual use. + * @param query the query document that specifies the criteria used to find a record. + * @param collation collation. * @param entityClass the parameterized type of the returned list. * @return the List of converted objects. */ - protected Mono doFindAndRemove(String collectionName, Document query, Document fields, Document sort, + protected Mono doFindAndRemove(String collectionName, + CollectionPreparer> collectionPreparer, Document query, Document fields, Document sort, @Nullable Collation collation, Class entityClass) { if (LOGGER.isDebugEnabled()) { @@ -2284,12 +2314,13 @@ protected Mono doFindAndRemove(String collectionName, Document query, Doc MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); - return executeFindOneInternal( - new FindAndRemoveCallback(queryMapper.getMappedObject(query, entity), fields, sort, collation), + return executeFindOneInternal(new FindAndRemoveCallback(collectionPreparer, + queryMapper.getMappedObject(query, entity), fields, sort, collation), new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); } - protected Mono doFindAndModify(String collectionName, Document query, Document fields, Document sort, + protected Mono doFindAndModify(String collectionName, + CollectionPreparer> collectionPreparer, Document query, Document fields, Document sort, Class entityClass, UpdateDefinition update, FindAndModifyOptions options) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); @@ -2310,7 +2341,7 @@ protected Mono doFindAndModify(String collectionName, Document query, Doc } return executeFindOneInternal( - new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate, + new FindAndModifyCallback(collectionPreparer, mappedQuery, fields, sort, mappedUpdate, update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options), new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); }); @@ -2320,6 +2351,7 @@ protected Mono doFindAndModify(String collectionName, Document query, Doc * Customize this part for findAndReplace. * * @param collectionName The name of the collection to perform the operation in. + * @param collectionPreparer the preparer to prepare the collection for the actual use. * @param mappedQuery the query to look up documents. * @param mappedFields the fields to project the result to. * @param mappedSort the sort to be applied when executing the query. @@ -2332,20 +2364,22 @@ protected Mono doFindAndModify(String collectionName, Document query, Doc * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. * @since 2.1 */ - protected Mono doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, + protected Mono doFindAndReplace(String collectionName, + CollectionPreparer> collectionPreparer, Document mappedQuery, Document mappedFields, Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { EntityProjection projection = operations.introspectProjection(resultType, entityType); - return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement, - options, projection); + return doFindAndReplace(collectionName, collectionPreparer, mappedQuery, mappedFields, mappedSort, collation, + entityType, replacement, options, projection); } /** * Customize this part for findAndReplace. * * @param collectionName The name of the collection to perform the operation in. + * @param collectionPreparer the preparer to prepare the collection for the actual use. * @param mappedQuery the query to look up documents. * @param mappedFields the fields to project the result to. * @param mappedSort the sort to be applied when executing the query. @@ -2358,7 +2392,8 @@ protected Mono doFindAndReplace(String collectionName, Document mappedQue * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. * @since 3.4 */ - private Mono doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, + private Mono doFindAndReplace(String collectionName, + CollectionPreparer> collectionPreparer, Document mappedQuery, Document mappedFields, Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, EntityProjection projection) { @@ -2372,8 +2407,8 @@ private Mono doFindAndReplace(String collectionName, Document mappedQuery serializeToJsonSafely(replacement), collectionName)); } - return executeFindOneInternal( - new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), + return executeFindOneInternal(new FindAndReplaceCallback(collectionPreparer, mappedQuery, mappedFields, + mappedSort, replacement, collation, options), new ProjectingReadCallback<>(this.mongoConverter, projection, collectionName), collectionName); }); @@ -2451,7 +2486,12 @@ protected void ensureNotCollectionLike(@Nullable Object source) { * @param collection */ protected MongoCollection prepareCollection(MongoCollection collection) { - return this.readPreference != null ? collection.withReadPreference(readPreference) : collection; + + if (this.readPreference != null && this.readPreference != collection.getReadPreference()) { + return collection.withReadPreference(readPreference); + } + + return collection; } /** @@ -2621,11 +2661,14 @@ private Document getMappedSortObject(Query query, Class type) { */ private static class FindOneCallback implements ReactiveCollectionCallback { + private final CollectionPreparer> collectionPreparer; private final Document query; private final Optional fields; private final FindPublisherPreparer preparer; - FindOneCallback(Document query, @Nullable Document fields, FindPublisherPreparer preparer) { + FindOneCallback(CollectionPreparer> collectionPreparer, Document query, + @Nullable Document fields, FindPublisherPreparer preparer) { + this.collectionPreparer = collectionPreparer; this.query = query; this.fields = Optional.ofNullable(fields); this.preparer = preparer; @@ -2642,7 +2685,8 @@ public Publisher doInCollection(MongoCollection collection) serializeToJsonSafely(fields.orElseGet(Document::new)), collection.getNamespace().getFullName())); } - FindPublisher publisher = preparer.initiateFind(collection, col -> col.find(query, Document.class)); + FindPublisher publisher = preparer.initiateFind(collectionPreparer.prepare(collection), + col -> col.find(query, Document.class)); if (fields.isPresent()) { publisher = publisher.projection(fields.get()); @@ -2660,15 +2704,17 @@ public Publisher doInCollection(MongoCollection collection) */ private static class FindCallback implements ReactiveCollectionQueryCallback { + private final CollectionPreparer> collectionPreparer; + private final @Nullable Document query; private final @Nullable Document fields; - FindCallback(@Nullable Document query) { - this(query, null); + FindCallback(CollectionPreparer> collectionPreparer, @Nullable Document query) { + this(collectionPreparer, query, null); } - FindCallback(Document query, Document fields) { - + FindCallback(CollectionPreparer> collectionPreparer, Document query, Document fields) { + this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; } @@ -2676,11 +2722,12 @@ private static class FindCallback implements ReactiveCollectionQueryCallback doInCollection(MongoCollection collection) { + MongoCollection collectionToUse = collectionPreparer.prepare(collection); FindPublisher findPublisher; if (ObjectUtils.isEmpty(query)) { - findPublisher = collection.find(Document.class); + findPublisher = collectionToUse.find(Document.class); } else { - findPublisher = collection.find(query, Document.class); + findPublisher = collectionToUse.find(query, Document.class); } if (ObjectUtils.isEmpty(fields)) { @@ -2699,13 +2746,15 @@ public FindPublisher doInCollection(MongoCollection collecti */ private static class FindAndRemoveCallback implements ReactiveCollectionCallback { + private final CollectionPreparer> collectionPreparer; private final Document query; private final Document fields; private final Document sort; private final Optional collation; - FindAndRemoveCallback(Document query, Document fields, Document sort, @Nullable Collation collation) { - + FindAndRemoveCallback(CollectionPreparer> collectionPreparer, Document query, + Document fields, Document sort, @Nullable Collation collation) { + this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; this.sort = sort; @@ -2719,7 +2768,7 @@ public Publisher doInCollection(MongoCollection collection) FindOneAndDeleteOptions findOneAndDeleteOptions = convertToFindOneAndDeleteOptions(fields, sort); collation.map(Collation::toMongoCollation).ifPresent(findOneAndDeleteOptions::collation); - return collection.findOneAndDelete(query, findOneAndDeleteOptions); + return collectionPreparer.prepare(collection).findOneAndDelete(query, findOneAndDeleteOptions); } } @@ -2728,6 +2777,7 @@ public Publisher doInCollection(MongoCollection collection) */ private static class FindAndModifyCallback implements ReactiveCollectionCallback { + private final CollectionPreparer> collectionPreparer; private final Document query; private final Document fields; private final Document sort; @@ -2735,9 +2785,10 @@ private static class FindAndModifyCallback implements ReactiveCollectionCallback private final List arrayFilters; private final FindAndModifyOptions options; - FindAndModifyCallback(Document query, Document fields, Document sort, Object update, List arrayFilters, - FindAndModifyOptions options) { + FindAndModifyCallback(CollectionPreparer> collectionPreparer, Document query, + Document fields, Document sort, Object update, List arrayFilters, FindAndModifyOptions options) { + this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; this.sort = sort; @@ -2750,21 +2801,22 @@ private static class FindAndModifyCallback implements ReactiveCollectionCallback public Publisher doInCollection(MongoCollection collection) throws MongoException, DataAccessException { + MongoCollection collectionToUse = collectionPreparer.prepare(collection); if (options.isRemove()) { FindOneAndDeleteOptions findOneAndDeleteOptions = convertToFindOneAndDeleteOptions(fields, sort); findOneAndDeleteOptions = options.getCollation().map(Collation::toMongoCollation) .map(findOneAndDeleteOptions::collation).orElse(findOneAndDeleteOptions); - return collection.findOneAndDelete(query, findOneAndDeleteOptions); + return collectionToUse.findOneAndDelete(query, findOneAndDeleteOptions); } FindOneAndUpdateOptions findOneAndUpdateOptions = convertToFindOneAndUpdateOptions(options, fields, sort, arrayFilters); if (update instanceof Document) { - return collection.findOneAndUpdate(query, (Document) update, findOneAndUpdateOptions); + return collectionToUse.findOneAndUpdate(query, (Document) update, findOneAndUpdateOptions); } else if (update instanceof List) { - return collection.findOneAndUpdate(query, (List) update, findOneAndUpdateOptions); + return collectionToUse.findOneAndUpdate(query, (List) update, findOneAndUpdateOptions); } return Flux @@ -2803,6 +2855,7 @@ private static FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndM */ private static class FindAndReplaceCallback implements ReactiveCollectionCallback { + private final CollectionPreparer> collectionPreparer; private final Document query; private final Document fields; private final Document sort; @@ -2810,9 +2863,10 @@ private static class FindAndReplaceCallback implements ReactiveCollectionCallbac private final @Nullable com.mongodb.client.model.Collation collation; private final FindAndReplaceOptions options; - FindAndReplaceCallback(Document query, Document fields, Document sort, Document update, - com.mongodb.client.model.Collation collation, FindAndReplaceOptions options) { - + FindAndReplaceCallback(CollectionPreparer> collectionPreparer, Document query, + Document fields, Document sort, Document update, com.mongodb.client.model.Collation collation, + FindAndReplaceOptions options) { + this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; this.sort = sort; @@ -2826,7 +2880,7 @@ public Publisher doInCollection(MongoCollection collection) throws MongoException, DataAccessException { FindOneAndReplaceOptions findOneAndReplaceOptions = convertToFindOneAndReplaceOptions(options, fields, sort); - return collection.findOneAndReplace(query, update, findOneAndReplaceOptions); + return collectionPreparer.prepare(collection).findOneAndReplace(query, update, findOneAndReplaceOptions); } private FindOneAndReplaceOptions convertToFindOneAndReplaceOptions(FindAndReplaceOptions options, Document fields, @@ -3092,11 +3146,6 @@ public FindPublisher prepare(FindPublisher findPublisher) { return findPublisherToUse; } - @Override - public ReadPreference getReadPreference() { - return query.getMeta().getFlags().contains(CursorOption.SECONDARY_READS) ? ReadPreference.primaryPreferred() - : null; - } } class TailingQueryFindPublisherPreparer extends QueryFindPublisherPreparer { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java new file mode 100644 index 0000000000..a916220d0a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java @@ -0,0 +1,46 @@ +/* + * 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. + * 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.core; + +import org.springframework.lang.Nullable; + +import com.mongodb.ReadConcern; + +/** + * Interface to be implemented by any object that wishes to expose the {@link ReadConcern}. + *

+ * Typically implemented by cursor or query preparer objects. + * + * @author Mark Paluch + * @since 4.1 + * @see org.springframework.data.mongodb.core.query.Query + * @see org.springframework.data.mongodb.core.aggregation.AggregationOptions + */ +public interface ReadConcernAware { + + /** + * @return {@literal true} if a {@link ReadConcern} is set. + */ + default boolean hasReadConcern() { + return getReadConcern() != null; + } + + /** + * @return the {@link ReadConcern} to apply or {@literal null} if none set. + */ + @Nullable + ReadConcern getReadConcern(); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java index 334a9d5c52..09c87af054 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java @@ -27,6 +27,8 @@ * @author Christoph Strobl * @author Mark Paluch * @since 2.2 + * @see org.springframework.data.mongodb.core.query.Query + * @see org.springframework.data.mongodb.core.aggregation.AggregationOptions */ public interface ReadPreferenceAware { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java index dbb5f49f58..f87a656b77 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java @@ -19,11 +19,16 @@ import java.util.Optional; import org.bson.Document; +import org.springframework.data.mongodb.core.ReadConcernAware; +import org.springframework.data.mongodb.core.ReadPreferenceAware; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; + /** * Holds a set of configurable aggregation options that can be used within an aggregation pipeline. A list of support * aggregation options can be found in the MongoDB reference documentation @@ -39,7 +44,7 @@ * @see TypedAggregation#withOptions(AggregationOptions) * @since 1.6 */ -public class AggregationOptions { +public class AggregationOptions implements ReadConcernAware, ReadPreferenceAware { private static final String BATCH_SIZE = "batchSize"; private static final String CURSOR = "cursor"; @@ -56,6 +61,10 @@ public class AggregationOptions { private final Optional collation; private final Optional comment; private final Optional hint; + + private Optional readConcern; + + private Optional readPreference; private Duration maxTime = Duration.ZERO; private ResultOptions resultOptions = ResultOptions.READ; private DomainTypeMapping domainTypeMapping = DomainTypeMapping.RELAXED; @@ -123,6 +132,8 @@ private AggregationOptions(boolean allowDiskUse, boolean explain, @Nullable Docu this.collation = Optional.ofNullable(collation); this.comment = Optional.ofNullable(comment); this.hint = Optional.ofNullable(hint); + this.readConcern = Optional.empty(); + this.readPreference = Optional.empty(); } /** @@ -268,6 +279,26 @@ public Optional getHintObject() { return hint; } + @Override + public boolean hasReadConcern() { + return readConcern.isPresent(); + } + + @Override + public ReadConcern getReadConcern() { + return readConcern.orElse(null); + } + + @Override + public boolean hasReadPreference() { + return readPreference.isPresent(); + } + + @Override + public ReadPreference getReadPreference() { + return readPreference.orElse(null); + } + /** * @return the time limit for processing. {@link Duration#ZERO} is used for the default unbounded behavior. * @since 3.0 @@ -385,6 +416,8 @@ public static class Builder { private @Nullable Collation collation; private @Nullable String comment; private @Nullable Object hint; + private @Nullable ReadConcern readConcern; + private @Nullable ReadPreference readPreference; private @Nullable Duration maxTime; private @Nullable ResultOptions resultOptions; private @Nullable DomainTypeMapping domainTypeMapping; @@ -490,6 +523,32 @@ public Builder hint(@Nullable String indexName) { return this; } + /** + * Define a {@link ReadConcern} to apply to the aggregation. + * + * @param readConcern can be {@literal null}. + * @return this. + * @since 4.1 + */ + public Builder readConcern(@Nullable ReadConcern readConcern) { + + this.readConcern = readConcern; + return this; + } + + /** + * Define a {@link ReadPreference} to apply to the aggregation. + * + * @param readPreference can be {@literal null}. + * @return this. + * @since 4.1 + */ + public Builder readPreference(@Nullable ReadPreference readPreference) { + + this.readPreference = readPreference; + return this; + } + /** * Set the time limit for processing. * @@ -573,6 +632,12 @@ public AggregationOptions build() { if (domainTypeMapping != null) { options.domainTypeMapping = domainTypeMapping; } + if (readConcern != null) { + options.readConcern = Optional.of(readConcern); + } + if (readPreference != null) { + options.readPreference = Optional.of(readPreference); + } return options; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java index 421e367871..0f5bd3d87e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java @@ -536,6 +536,11 @@ public NearQuery query(Query query) { return this; } + @Nullable + public Query getQuery() { + return query; + } + /** * @return the number of elements to skip. */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java index 23c0f890dd..912e7d5cea 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java @@ -34,10 +34,16 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; +import org.springframework.data.mongodb.core.ReadConcernAware; +import org.springframework.data.mongodb.core.ReadPreferenceAware; +import org.springframework.data.mongodb.core.query.Meta.CursorOption; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; + /** * MongoDB Query object representing criteria, projection, sorting and query hints. * @@ -48,7 +54,7 @@ * @author Mark Paluch * @author Anton Barkan */ -public class Query { +public class Query implements ReadConcernAware, ReadPreferenceAware { private static final String RESTRICTED_TYPES_KEY = "_$RESTRICTED_TYPES"; @@ -58,6 +64,9 @@ public class Query { private Sort sort = Sort.unsorted(); private long skip; private int limit; + private @Nullable ReadConcern readConcern; + private @Nullable ReadPreference readPreference; + private @Nullable String hint; private Meta meta = new Meta(); @@ -160,6 +169,59 @@ public Query withHint(String hint) { return this; } + /** + * Configures the query to use the given {@link ReadConcern} when being executed. + * + * @param readConcern must not be {@literal null}. + * @return this. + * @since 3.1 + */ + public Query withReadConcern(ReadConcern readConcern) { + + Assert.notNull(readConcern, "ReadConcern must not be null"); + this.readConcern = readConcern; + return this; + } + + /** + * Configures the query to use the given {@link ReadPreference} when being executed. + * + * @param readPreference must not be {@literal null}. + * @return this. + * @since 4.1 + */ + public Query withReadPreference(ReadPreference readPreference) { + + Assert.notNull(readPreference, "ReadPreference must not be null"); + this.readPreference = readPreference; + return this; + } + + @Override + public boolean hasReadConcern() { + return this.readConcern != null; + } + + @Override + public ReadConcern getReadConcern() { + return this.readConcern; + } + + @Override + public boolean hasReadPreference() { + return this.readPreference != null || getMeta().getFlags().contains(CursorOption.SECONDARY_READS); + } + + @Override + public ReadPreference getReadPreference() { + + if (readPreference == null) { + return getMeta().getFlags().contains(CursorOption.SECONDARY_READS) ? ReadPreference.primaryPreferred() : null; + } + + return this.readPreference; + } + /** * Configures the query to use the given {@link Document hint} when being executed. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java index 0790e0aff8..630946f50f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java @@ -64,6 +64,7 @@ static Query decorateSort(Query query, Document defaultSort) { combinedSort.putAll((Document) invocation.proceed()); return combinedSort; }); + factory.setInterfaces(new Class[0]); return (Query) factory.getProxy(query.getClass().getClassLoader()); } @@ -113,7 +114,7 @@ static int indexOfAssignableParameter(Class type, List> parameters) if(parameters.isEmpty()) { return -1; } - + int i = 0; for(Class parameterType : parameters) { if(ClassUtils.isAssignable(type, parameterType)) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 850797880d..ed4064a12d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -108,6 +108,7 @@ import com.mongodb.MongoClientSettings; import com.mongodb.MongoException; import com.mongodb.MongoNamespace; +import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; import com.mongodb.ServerAddress; import com.mongodb.ServerCursor; @@ -120,16 +121,7 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; -import com.mongodb.client.model.CountOptions; -import com.mongodb.client.model.CreateCollectionOptions; -import com.mongodb.client.model.DeleteOptions; -import com.mongodb.client.model.FindOneAndDeleteOptions; -import com.mongodb.client.model.FindOneAndReplaceOptions; -import com.mongodb.client.model.FindOneAndUpdateOptions; -import com.mongodb.client.model.MapReduceAction; -import com.mongodb.client.model.ReplaceOptions; -import com.mongodb.client.model.TimeSeriesGranularity; -import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.*; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; @@ -182,6 +174,7 @@ void beforeEach() { when(collection.estimatedDocumentCount(any())).thenReturn(1L); when(collection.getNamespace()).thenReturn(new MongoNamespace("db.mock-collection")); when(collection.aggregate(any(List.class), any())).thenReturn(aggregateIterable); + when(collection.withReadConcern(any())).thenReturn(collection); when(collection.withReadPreference(any())).thenReturn(collection); when(collection.replaceOne(any(), any(), any(ReplaceOptions.class))).thenReturn(updateResult); when(collection.withWriteConcern(any())).thenReturn(collectionWithWriteConcern); @@ -478,6 +471,34 @@ void aggregateShouldIgnoreReadPreferenceWhenNotSet() { verify(collection, never()).withReadPreference(any()); } + @Test // GH-4277 + void aggregateShouldHonorOptionsReadConcernWhenSet() { + + AggregationOptions options = AggregationOptions.builder().readConcern(ReadConcern.SNAPSHOT).build(); + template.aggregate(newAggregation(Aggregation.unwind("foo")).withOptions(options), "collection-1", Wrapper.class); + + verify(collection).withReadConcern(ReadConcern.SNAPSHOT); + } + + @Test // GH-4277 + void aggregateShouldHonorOptionsReadPreferenceWhenSet() { + + AggregationOptions options = AggregationOptions.builder().readPreference(ReadPreference.secondary()).build(); + template.aggregate(newAggregation(Aggregation.unwind("foo")).withOptions(options), "collection-1", Wrapper.class); + + verify(collection).withReadPreference(ReadPreference.secondary()); + } + + @Test // GH-4277 + void aggregateStreamShouldHonorOptionsReadPreferenceWhenSet() { + + AggregationOptions options = AggregationOptions.builder().readPreference(ReadPreference.secondary()).build(); + template.aggregateStream(newAggregation(Aggregation.unwind("foo")).withOptions(options), "collection-1", + Wrapper.class); + + verify(collection).withReadPreference(ReadPreference.secondary()); + } + @Test // DATAMONGO-2153 void aggregateShouldHonorOptionsComment() { @@ -558,6 +579,19 @@ void geoNearShouldHonorReadPreferenceWhenSet() { verify(collection).withReadPreference(eq(ReadPreference.secondary())); } + @Test // GH-4277 + void geoNearShouldHonorReadPreferenceFromQuery() { + + NearQuery query = NearQuery.near(new Point(1, 1)); + + Query inner = new Query(); + inner.withReadPreference(ReadPreference.secondary()); + query.query(inner); + template.geoNear(query, Wrapper.class); + + verify(collection).withReadPreference(eq(ReadPreference.secondary())); + } + @Test // DATAMONGO-1166, DATAMONGO-2264 void geoNearShouldIgnoreReadPreferenceWhenNotSet() { @@ -802,6 +836,24 @@ void executeQueryShouldUseBatchSizeWhenPresent() { verify(findIterable).batchSize(1234); } + @Test // GH-4277 + void findShouldUseReadConcernWhenPresent() { + + template.find(new BasicQuery("{'foo' : 'bar'}").withReadConcern(ReadConcern.SNAPSHOT), + AutogenerateableId.class); + + verify(collection).withReadConcern(ReadConcern.SNAPSHOT); + } + + @Test // GH-4277 + void findShouldUseReadPreferenceWhenPresent() { + + template.find(new BasicQuery("{'foo' : 'bar'}").withReadPreference(ReadPreference.secondary()), + AutogenerateableId.class); + + verify(collection).withReadPreference(ReadPreference.secondary()); + } + @Test // DATAMONGO-1518 void executeQueryShouldUseCollationWhenPresent() { @@ -1048,7 +1100,8 @@ void countShouldApplyQueryHintAsIndexNameIfPresent() { @Test // DATAMONGO-1733 void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonProjection.class, + template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, + PersonProjection.class, CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("firstname", 1))); @@ -1057,7 +1110,8 @@ void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { @Test // DATAMONGO-1733 void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { - template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, PersonProjection.class, + template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document("bar", 1), Person.class, + PersonProjection.class, CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("bar", 1))); @@ -1066,7 +1120,8 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { @Test // DATAMONGO-1733 void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonSpELProjection.class, + template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, + PersonSpELProjection.class, CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT)); @@ -1075,7 +1130,8 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { @Test // DATAMONGO-1733, DATAMONGO-2041 void appliesFieldsToDtoProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, + template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, + Jedi.class, CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("firstname", 1))); @@ -1084,7 +1140,8 @@ void appliesFieldsToDtoProjection() { @Test // DATAMONGO-1733 void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { - template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, Jedi.class, + template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document("bar", 1), Person.class, + Jedi.class, CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("bar", 1))); @@ -1093,7 +1150,8 @@ void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { @Test // DATAMONGO-1733 void doesNotApplyFieldsWhenTargetIsNotAProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, Person.class, + template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, + Person.class, CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT)); @@ -1102,7 +1160,8 @@ void doesNotApplyFieldsWhenTargetIsNotAProjection() { @Test // DATAMONGO-1733 void doesNotApplyFieldsWhenTargetExtendsDomainType() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonExtended.class, + template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, + PersonExtended.class, CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT)); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index df1bb84466..962685890c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -23,8 +23,6 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.springframework.data.mongodb.core.MongoTemplateUnitTests.Wrapper; -import org.springframework.data.mongodb.core.aggregation.Aggregation; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -56,7 +54,6 @@ import org.mockito.quality.Strictness; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; - import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; @@ -100,6 +97,7 @@ import org.springframework.util.CollectionUtils; import com.mongodb.MongoClientSettings; +import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; import com.mongodb.client.model.CountOptions; import com.mongodb.client.model.CreateCollectionOptions; @@ -168,6 +166,7 @@ void beforeEach() { when(db.runCommand(any(), any(Class.class))).thenReturn(runCommandPublisher); when(db.createCollection(any(), any(CreateCollectionOptions.class))).thenReturn(runCommandPublisher); when(collection.withReadPreference(any())).thenReturn(collection); + when(collection.withReadConcern(any())).thenReturn(collection); when(collection.find(any(Class.class))).thenReturn(findPublisher); when(collection.find(any(Document.class), any(Class.class))).thenReturn(findPublisher); when(collection.aggregate(anyList())).thenReturn(aggregatePublisher); @@ -388,8 +387,8 @@ void geoNearShouldUseCollationWhenPresent() { @Test // DATAMONGO-1719 void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonProjection.class, - FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, + PersonProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("firstname", 1))); } @@ -397,8 +396,8 @@ void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { @Test // DATAMONGO-1719 void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { - template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, PersonProjection.class, - FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document("bar", 1), Person.class, + PersonProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("bar", 1))); } @@ -406,8 +405,8 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { @Test // DATAMONGO-1719 void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonSpELProjection.class, - FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, + PersonSpELProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } @@ -415,8 +414,8 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { @Test // DATAMONGO-1719, DATAMONGO-2041 void appliesFieldsToDtoProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, - FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, + Jedi.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("firstname", 1))); } @@ -424,8 +423,8 @@ void appliesFieldsToDtoProjection() { @Test // DATAMONGO-1719 void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { - template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, Jedi.class, - FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document("bar", 1), Person.class, + Jedi.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("bar", 1))); } @@ -433,8 +432,8 @@ void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { @Test // DATAMONGO-1719 void doesNotApplyFieldsWhenTargetIsNotAProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, Person.class, - FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, + Person.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } @@ -442,8 +441,8 @@ void doesNotApplyFieldsWhenTargetIsNotAProjection() { @Test // DATAMONGO-1719 void doesNotApplyFieldsWhenTargetExtendsDomainType() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonExtended.class, - FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, + PersonExtended.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } @@ -632,6 +631,26 @@ void aggreateShouldUseDefaultCollationIfPresent() { verify(aggregatePublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("de_AT").build())); } + @Test // GH-4277 + void aggreateShouldUseReadConcern() { + + AggregationOptions options = AggregationOptions.builder().readConcern(ReadConcern.SNAPSHOT).build(); + template.aggregate(newAggregation(Sith.class, project("id")).withOptions(options), AutogenerateableId.class, + Document.class).subscribe(); + + verify(collection).withReadConcern(ReadConcern.SNAPSHOT); + } + + @Test // GH-4286 + void aggreateShouldUseReadReadPreference() { + + AggregationOptions options = AggregationOptions.builder().readPreference(ReadPreference.primaryPreferred()).build(); + template.aggregate(newAggregation(Sith.class, project("id")).withOptions(options), AutogenerateableId.class, + Document.class).subscribe(); + + verify(collection).withReadPreference(ReadPreference.primaryPreferred()); + } + @Test // DATAMONGO-1854 void aggreateShouldUseCollationFromOptionsEvenIfDefaultCollationIsPresent() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/QueryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/QueryTests.java index e54ab48634..c4f7deed12 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/QueryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/QueryTests.java @@ -342,7 +342,10 @@ void queryOfShouldWorkOnProxiedObjects() { source.limit(10); source.setSortObject(new Document("_id", 1)); - Query target = Query.of((Query) new ProxyFactory(source).getProxy()); + ProxyFactory proxyFactory = new ProxyFactory(source); + proxyFactory.setInterfaces(new Class[0]); + + Query target = Query.of((Query) proxyFactory.getProxy()); compareQueries(target, source); }