diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java index b0fbd09b5..9f4fa09d6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java @@ -15,9 +15,15 @@ */ package org.springframework.data.mongodb.core; -import org.bson.Document; +import java.util.function.Function; +import org.bson.Document; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import com.mongodb.ReadPreference; import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; /** * Simple callback interface to allow customization of a {@link FindIterable}. @@ -25,7 +31,14 @@ import com.mongodb.client.FindIterable; * @author Oliver Gierke * @author Christoph Strobl */ -interface CursorPreparer { +interface CursorPreparer extends ReadPreferenceAware { + + /** + * Default {@link CursorPreparer} just passing on the given {@link FindIterable}. + * + * @since 2.2 + */ + CursorPreparer NO_OP_PREPARER = (iterable -> iterable); /** * Prepare the given cursor (apply limits, skips and so on). Returns the prepared cursor. @@ -33,4 +46,37 @@ interface CursorPreparer { * @param cursor */ FindIterable prepare(FindIterable cursor); + + /** + * Apply query specific settings to {@link MongoCollection} and initate a find operation returning a + * {@link FindIterable} via the given {@link Function find} function. + * + * @param collection must not be {@literal null}. + * @param find must not be {@literal null}. + * @return + * @throws IllegalArgumentException if one of the required arguments is {@literal null}. + * @since 2.2 + */ + default FindIterable initiateFind(MongoCollection collection, + Function, FindIterable> find) { + + Assert.notNull(collection, "Collection must not be null!"); + Assert.notNull(find, "Find function must not be null!"); + + if (hasReadPreferences()) { + collection = collection.withReadPreference(getReadPreference()); + } + + return prepare(find.apply(collection)); + } + + /** + * @return the {@link ReadPreference} to apply or {@literal null} if none defined. + * @since 2.2 + */ + @Override + @Nullable + default ReadPreference getReadPreference() { + return null; + } } 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 e33146f46..06fce43e5 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import com.mongodb.ReadPreference; import lombok.AccessLevel; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -267,6 +268,11 @@ class ExecutableFindOperationSupport implements ExecutableFindOperation { this.limit = Optional.of(limit); return this; } + + @Override + public ReadPreference getReadPreference() { + return delegate.getReadPreference(); + } } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java index 19f3ea49c..1252e7286 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java @@ -15,14 +15,36 @@ */ package org.springframework.data.mongodb.core; +import java.util.function.Function; + +import org.bson.Document; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import com.mongodb.ReadPreference; import com.mongodb.reactivestreams.client.FindPublisher; +import com.mongodb.reactivestreams.client.MongoCollection; /** * Simple callback interface to allow customization of a {@link FindPublisher}. * * @author Mark Paluch + * @author Christoph Strobl */ -interface FindPublisherPreparer { +interface FindPublisherPreparer extends ReadPreferenceAware { + + /** + * Default {@link FindPublisherPreparer} just passing on the given {@link FindPublisher}. + * + * @since 2.2 + */ + FindPublisherPreparer NO_OP_PREPARER = new FindPublisherPreparer() { + + @Override + public FindPublisher prepare(FindPublisher findPublisher) { + return findPublisher; + } + }; /** * Prepare the given cursor (apply limits, skips and so on). Returns the prepared cursor. @@ -30,4 +52,37 @@ interface FindPublisherPreparer { * @param findPublisher must not be {@literal null}. */ FindPublisher prepare(FindPublisher findPublisher); + + /** + * Apply query specific settings to {@link MongoCollection} and initate a find operation returning a + * {@link FindPublisher} via the given {@link Function find} function. + * + * @param collection must not be {@literal null}. + * @param find must not be {@literal null}. + * @return + * @throws IllegalArgumentException if one of the required arguments is {@literal null}. + * @since 2.2 + */ + default FindPublisher initiateFind(MongoCollection collection, + Function, FindPublisher> find) { + + Assert.notNull(collection, "Collection must not be null!"); + Assert.notNull(find, "Find function must not be null!"); + + if (hasReadPreferences()) { + collection = collection.withReadPreference(getReadPreference()); + } + + return prepare(find.apply(collection)); + } + + /** + * @return the {@link ReadPreference} to apply or {@literal null} if none defined. + * @since 2.2 + */ + @Override + @Nullable + default ReadPreference getReadPreference() { + return null; + } } 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 e2fe7f05f..587284e7b 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 @@ -105,6 +105,7 @@ import org.springframework.data.mongodb.core.mapreduce.MapReduceResults; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Criteria; 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.Update; @@ -447,8 +448,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document mappedFields = getMappedFieldsObject(query.getFieldsObject(), persistentEntity, returnType); Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), persistentEntity); - FindIterable cursor = new QueryCursorPreparer(query, entityType) - .prepare(collection.find(mappedQuery, Document.class).projection(mappedFields)); + FindIterable cursor = new QueryCursorPreparer(query, entityType).initiateFind(collection, + col -> col.find(mappedQuery, Document.class).projection(mappedFields)); return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator, new ProjectingReadCallback<>(mongoConverter, entityType, returnType, collectionName)); @@ -538,8 +539,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, sortObject, fieldsObject, collectionName); } - this.executeQueryInternal(new FindCallback(queryObject, fieldsObject, null), preparer, documentCallbackHandler, - collectionName); + this.executeQueryInternal(new FindCallback(queryObject, fieldsObject, null), + preparer != null ? preparer : CursorPreparer.NO_OP_PREPARER, documentCallbackHandler, collectionName); } /* @@ -777,8 +778,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(mode, "BulkMode must not be null!"); Assert.hasText(collectionName, "Collection name must not be null or empty!"); - DefaultBulkOperations operations = new DefaultBulkOperations(this, collectionName, new BulkOperationContext(mode, - Optional.ofNullable(getPersistentEntity(entityType)), queryMapper, updateMapper, eventPublisher, entityCallbacks)); + DefaultBulkOperations operations = new DefaultBulkOperations(this, collectionName, + new BulkOperationContext(mode, Optional.ofNullable(getPersistentEntity(entityType)), queryMapper, updateMapper, + eventPublisher, entityCallbacks)); operations.setExceptionTranslator(exceptionTranslator); operations.setDefaultWriteConcern(writeConcern); @@ -814,8 +816,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, if (ObjectUtils.isEmpty(query.getSortObject())) { return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), - operations.forType(entityClass).getCollation(query).map(Collation::toMongoCollation).orElse(null), - entityClass); + new QueryCursorPreparer(query, entityClass), entityClass); } else { query.limit(1); List results = find(query, entityClass, collectionName); @@ -933,6 +934,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, serializeToJsonSafely(mappedQuery), field, collectionName); } + QueryCursorPreparer preparer = new QueryCursorPreparer(query, entityClass); + if (preparer.hasReadPreferences()) { + collection = collection.withReadPreference(preparer.getReadPreference()); + } + DistinctIterable iterable = collection.distinct(mappedFieldName, mappedQuery, mongoDriverCompatibleType); return operations.forType(entityClass) // @@ -1007,8 +1013,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(collectionName, "CollectionName must not be null!"); Assert.notNull(returnType, "ReturnType must not be null!"); - String collection = StringUtils.hasText(collectionName) ? collectionName - : getCollectionName(domainType); + String collection = StringUtils.hasText(collectionName) ? collectionName : getCollectionName(domainType); String distanceField = operations.nearQueryDistanceFieldName(domainType); Aggregation $geoNear = TypedAggregation.newAggregation(domainType, Aggregation.geoNear(near, distanceField)) @@ -1039,8 +1044,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Nullable @Override public T findAndModify(Query query, Update update, Class entityClass) { - return findAndModify(query, update, new FindAndModifyOptions(), entityClass, - getCollectionName(entityClass)); + return findAndModify(query, update, new FindAndModifyOptions(), entityClass, getCollectionName(entityClass)); } @Nullable @@ -1296,8 +1300,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(batchToSave, "BatchToSave must not be null!"); - return (Collection) doInsertBatch(getCollectionName(entityClass), batchToSave, - this.mongoConverter); + return (Collection) doInsertBatch(getCollectionName(entityClass), batchToSave, this.mongoConverter); } @Override @@ -1795,7 +1798,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return executeFindMultiInternal( new FindCallback(new Document(), new Document(), operations.forType(entityClass).getCollation().map(Collation::toMongoCollation).orElse(null)), - null, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), collectionName); + CursorPreparer.NO_OP_PREPARER, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), + collectionName); } @Override @@ -2435,7 +2439,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @return the {@link List} of converted objects. */ protected T doFindOne(String collectionName, Document query, Document fields, Class entityClass) { - return doFindOne(collectionName, query, fields, null, entityClass); + return doFindOne(collectionName, query, fields, CursorPreparer.NO_OP_PREPARER, entityClass); } /** @@ -2446,12 +2450,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @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. * @return the {@link List} of converted objects. * @since 2.2 */ @SuppressWarnings("ConstantConditions") - protected T doFindOne(String collectionName, Document query, Document fields, - @Nullable com.mongodb.client.model.Collation collation, Class entityClass) { + protected T doFindOne(String collectionName, Document query, Document fields, CursorPreparer preparer, + Class entityClass) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); Document mappedQuery = queryMapper.getMappedObject(query, entity); @@ -2462,7 +2467,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, mappedFields, entityClass, collectionName); } - return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields, collation), + return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields, preparer), new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); } @@ -2513,8 +2518,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, serializeToJsonSafely(mappedQuery), mappedFields, entityClass, collectionName); } - return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields, null), preparer, objectCallback, - collectionName); + return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields, null), + preparer != null ? preparer : CursorPreparer.NO_OP_PREPARER, objectCallback, collectionName); } /** @@ -2736,6 +2741,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, DocumentCallback objectCallback, String collectionName) { try { + T result = objectCallback .doWith(collectionCallback.doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName))); return result; @@ -2763,7 +2769,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @return */ private List executeFindMultiInternal(CollectionCallback> collectionCallback, - @Nullable CursorPreparer preparer, DocumentCallback objectCallback, String collectionName) { + CursorPreparer preparer, DocumentCallback objectCallback, String collectionName) { try { @@ -2771,14 +2777,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, try { - FindIterable iterable = collectionCallback - .doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName)); - - if (preparer != null) { - iterable = preparer.prepare(iterable); - } - - cursor = iterable.iterator(); + cursor = preparer + .initiateFind(getAndPrepareCollection(doGetDatabase(), collectionName), collectionCallback::doInCollection) + .iterator(); List result = new ArrayList<>(); @@ -2800,21 +2801,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } private void executeQueryInternal(CollectionCallback> collectionCallback, - @Nullable CursorPreparer preparer, DocumentCallbackHandler callbackHandler, String collectionName) { + CursorPreparer preparer, DocumentCallbackHandler callbackHandler, String collectionName) { try { MongoCursor cursor = null; try { - FindIterable iterable = collectionCallback - .doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName)); - if (preparer != null) { - iterable = preparer.prepare(iterable); - } - - cursor = iterable.iterator(); + cursor = preparer + .initiateFind(getAndPrepareCollection(doGetDatabase(), collectionName), collectionCallback::doInCollection) + .iterator(); while (cursor.hasNext()) { callbackHandler.processDocument(cursor.next()); @@ -2908,21 +2905,19 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private final Document query; private final Optional fields; - private final @Nullable com.mongodb.client.model.Collation collation; + private final CursorPreparer cursorPreparer; - public FindOneCallback(Document query, Document fields, @Nullable com.mongodb.client.model.Collation collation) { + FindOneCallback(Document query, Document fields, CursorPreparer preparer) { this.query = query; this.fields = Optional.of(fields).filter(it -> !ObjectUtils.isEmpty(fields)); - this.collation = collation; + this.cursorPreparer = preparer; } + @Override public Document doInCollection(MongoCollection collection) throws MongoException, DataAccessException { - FindIterable iterable = collection.find(query, Document.class); - if (collation != null) { - iterable = iterable.collation(collation); - } + FindIterable iterable = cursorPreparer.initiateFind(collection, col -> col.find(query, Document.class)); if (LOGGER.isDebugEnabled()) { @@ -3318,6 +3313,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, case PARTIAL: cursorToUse = cursorToUse.partial(true); break; + case SLAVE_OK: + break; default: throw new IllegalArgumentException(String.format("%s is no supported flag.", option)); } @@ -3330,6 +3327,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return cursorToUse; } + + @Override + public ReadPreference getReadPreference() { + return query.getMeta().getFlags().contains(CursorOption.SLAVE_OK) ? ReadPreference.primaryPreferred() : null; + } } /** 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 c04bf27df..458d8ee4b 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 @@ -102,6 +102,7 @@ import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCal import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; 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.Update; @@ -803,7 +804,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati if (ObjectUtils.isEmpty(query.getSortObject())) { return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass, - operations.forType(entityClass).getCollation(query).orElse(null)); + new QueryFindPublisherPreparer(query, entityClass)); } query.limit(1); @@ -891,7 +892,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati String idKey = operations.getIdPropertyName(entityClass); - return doFindOne(collectionName, new Document(idKey, id), null, entityClass, null); + return doFindOne(collectionName, new Document(idKey, id), null, entityClass, (Collation) null); } /* @@ -932,6 +933,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati serializeToJsonSafely(mappedQuery), field, collectionName); } + FindPublisherPreparer preparer = new QueryFindPublisherPreparer(query, entityClass); + if (preparer.hasReadPreferences()) { + collection = collection.withReadPreference(preparer.getReadPreference()); + } + DistinctPublisher publisher = collection.distinct(mappedFieldName, mappedQuery, mongoDriverCompatibleType); return operations.forType(entityClass).getCollation(query) // .map(Collation::toMongoCollation) // @@ -1981,7 +1987,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#findAll(java.lang.Class, java.lang.String) */ public Flux findAll(Class entityClass, String collectionName) { - return executeFindMultiInternal(new FindCallback(null), null, + return executeFindMultiInternal(new FindCallback(null), FindPublisherPreparer.NO_OP_PREPARER, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), collectionName); } @@ -2035,8 +2041,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati LOGGER.debug(String.format("find for class: %s in collection: %s", entityClass, collectionName)); return executeFindMultiInternal( - collection -> new FindCallback(null).doInCollection(collection).cursorType(CursorType.TailableAwait), null, - new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), collectionName); + collection -> new FindCallback(null).doInCollection(collection).cursorType(CursorType.TailableAwait), + FindPublisherPreparer.NO_OP_PREPARER, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), + collectionName); } return doFind(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass, @@ -2327,6 +2334,29 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati protected Mono doFindOne(String collectionName, Document query, @Nullable Document fields, Class entityClass, @Nullable Collation collation) { + return doFindOne(collectionName, query, fields, entityClass, new FindPublisherPreparer() { + @Override + public FindPublisher prepare(FindPublisher findPublisher) { + return collation != null ? findPublisher.collation(collation.toMongoCollation()) : findPublisher; + } + }); + } + + /** + * Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter. + * 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 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 modifying collection and publisher to fit the needs. + * @return the {@link List} of converted objects. + * @since 2.2 + */ + protected Mono doFindOne(String collectionName, Document query, @Nullable Document fields, + Class entityClass, FindPublisherPreparer preparer) { + MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); Document mappedQuery = queryMapper.getMappedObject(query, entity); Document mappedFields = fields == null ? null : queryMapper.getMappedObject(fields, entity); @@ -2336,7 +2366,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati serializeToJsonSafely(query), mappedFields, entityClass, collectionName)); } - return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields, collation), + return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields, preparer), new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); } @@ -2706,13 +2736,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @Nullable FindPublisherPreparer preparer, DocumentCallback objectCallback, String collectionName) { return createFlux(collectionName, collection -> { - - FindPublisher findPublisher = collectionCallback.doInCollection(collection); - - if (preparer != null) { - findPublisher = preparer.prepare(findPublisher); - } - return Flux.from(findPublisher).map(objectCallback::doWith); + return Flux.from(preparer.initiateFind(collection, collectionCallback::doInCollection)) + .map(objectCallback::doWith); }); } @@ -2784,37 +2809,36 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati * * @author Oliver Gierke * @author Thomas Risberg + * @author Christoph Strobl */ private static class FindOneCallback implements ReactiveCollectionCallback { private final Document query; private final Optional fields; - private final Optional collation; + private final FindPublisherPreparer preparer; - FindOneCallback(Document query, @Nullable Document fields, @Nullable Collation collation) { + FindOneCallback(Document query, @Nullable Document fields, FindPublisherPreparer preparer) { this.query = query; this.fields = Optional.ofNullable(fields); - this.collation = Optional.ofNullable(collation); + this.preparer = preparer; } @Override public Publisher doInCollection(MongoCollection collection) throws MongoException, DataAccessException { - FindPublisher publisher = collection.find(query, Document.class); - if (LOGGER.isDebugEnabled()) { LOGGER.debug("findOne using query: {} fields: {} in db.collection: {}", serializeToJsonSafely(query), serializeToJsonSafely(fields.orElseGet(Document::new)), collection.getNamespace().getFullName()); } + FindPublisher publisher = preparer.initiateFind(collection, col -> col.find(query, Document.class)); + if (fields.isPresent()) { publisher = publisher.projection(fields.get()); } - publisher = collation.map(Collation::toMongoCollation).map(publisher::collation).orElse(publisher); - return publisher.limit(1).first(); } } @@ -3221,6 +3245,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return findPublisherToUse; } + + @Override + public ReadPreference getReadPreference() { + return query.getMeta().getFlags().contains(CursorOption.SLAVE_OK) ? ReadPreference.primaryPreferred() : null; + } } class TailingQueryFindPublisherPreparer extends QueryFindPublisherPreparer { 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 new file mode 100644 index 000000000..8811d55a5 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 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.ReadPreference; + +/** + * @author Christoph Strobl + * @since 2.2 + */ +interface ReadPreferenceAware { + + /** + * @return {@literal true} if a {@link ReadPreference} is set. + */ + default boolean hasReadPreferences() { + return getReadPreference() != null; + } + + /** + * @return the {@link ReadPreference} to apply or {@literal null} if none set. + */ + @Nullable + ReadPreference getReadPreference(); +} 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 2e53081dc..61221f8c2 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 @@ -935,7 +935,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Test // DATAMONGO-1733 public void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonProjection.class, null); + template.doFind("star-wars", new Document(), new Document(), Person.class, PersonProjection.class, + CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("firstname", 1))); } @@ -943,7 +944,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Test // DATAMONGO-1733 public void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { - template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, PersonProjection.class, null); + template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, PersonProjection.class, + CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("bar", 1))); } @@ -951,7 +953,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Test // DATAMONGO-1733 public void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonSpELProjection.class, null); + template.doFind("star-wars", new Document(), new Document(), Person.class, PersonSpELProjection.class, + CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document())); } @@ -959,7 +962,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Test // DATAMONGO-1733, DATAMONGO-2041 public void appliesFieldsToDtoProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, null); + template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, + CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("firstname", 1))); } @@ -967,7 +971,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Test // DATAMONGO-1733 public void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { - template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, Jedi.class, null); + template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, Jedi.class, + CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("bar", 1))); } @@ -975,7 +980,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Test // DATAMONGO-1733 public void doesNotApplyFieldsWhenTargetIsNotAProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, Person.class, null); + template.doFind("star-wars", new Document(), new Document(), Person.class, Person.class, + CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document())); } @@ -983,7 +989,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Test // DATAMONGO-1733 public void doesNotApplyFieldsWhenTargetExtendsDomainType() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonExtended.class, null); + template.doFind("star-wars", new Document(), new Document(), Person.class, PersonExtended.class, + CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document())); } @@ -1637,6 +1644,37 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { Assertions.assertThat(ReflectionTestUtils.getField(template, "entityCallbacks")).isSameAs(callbacks); } + @Test // DATAMONGO-2344 + public void slaveOkQueryOptionShouldApplyPrimaryPreferredReadPreferenceForFind() { + + template.find(new Query().slaveOk(), AutogenerateableId.class); + + verify(collection).withReadPreference(eq(ReadPreference.primaryPreferred())); + } + + @Test // DATAMONGO-2344 + public void slaveOkQueryOptionShouldApplyPrimaryPreferredReadPreferenceForFindOne() { + + template.findOne(new Query().slaveOk(), AutogenerateableId.class); + + verify(collection).withReadPreference(eq(ReadPreference.primaryPreferred())); + } + + @Test // DATAMONGO-2344 + public void slaveOkQueryOptionShouldApplyPrimaryPreferredReadPreferenceForFindDistinct() { + + template.findDistinct(new Query().slaveOk(), "name", AutogenerateableId.class, String.class); + + verify(collection).withReadPreference(eq(ReadPreference.primaryPreferred())); + } + + @Test // DATAMONGO-2344 + public void slaveOkQueryOptionShouldApplyPrimaryPreferredReadPreferenceForStream() { + + template.stream(new Query().slaveOk(), AutogenerateableId.class); + verify(collection).withReadPreference(eq(ReadPreference.primaryPreferred())); + } + class AutogenerateableId { @Id BigInteger id; 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 01e6e7f7e..d42e759e7 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 @@ -40,7 +40,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.reactivestreams.Publisher; - import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.support.StaticApplicationContext; @@ -65,6 +64,7 @@ import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; +import com.mongodb.ReadPreference; import com.mongodb.client.model.CountOptions; import com.mongodb.client.model.CreateCollectionOptions; import com.mongodb.client.model.DeleteOptions; @@ -121,6 +121,7 @@ public class ReactiveMongoTemplateUnitTests { when(db.getCollection(any(), any())).thenReturn(collection); 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.find(any(Class.class))).thenReturn(findPublisher); when(collection.find(any(Document.class), any(Class.class))).thenReturn(findPublisher); when(collection.aggregate(anyList())).thenReturn(aggregatePublisher); @@ -324,8 +325,8 @@ public class ReactiveMongoTemplateUnitTests { @Test // DATAMONGO-1719 public void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonProjection.class, null) - .subscribe(); + template.doFind("star-wars", new Document(), new Document(), Person.class, PersonProjection.class, + FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("firstname", 1))); } @@ -333,8 +334,8 @@ public class ReactiveMongoTemplateUnitTests { @Test // DATAMONGO-1719 public void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { - template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, PersonProjection.class, null) - .subscribe(); + template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, PersonProjection.class, + FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("bar", 1))); } @@ -342,8 +343,8 @@ public class ReactiveMongoTemplateUnitTests { @Test // DATAMONGO-1719 public void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonSpELProjection.class, null) - .subscribe(); + template.doFind("star-wars", new Document(), new Document(), Person.class, PersonSpELProjection.class, + FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } @@ -351,7 +352,8 @@ public class ReactiveMongoTemplateUnitTests { @Test // DATAMONGO-1719, DATAMONGO-2041 public void appliesFieldsToDtoProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, null).subscribe(); + template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, + FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("firstname", 1))); } @@ -359,7 +361,8 @@ public class ReactiveMongoTemplateUnitTests { @Test // DATAMONGO-1719 public void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { - template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, Jedi.class, null).subscribe(); + template.doFind("star-wars", new Document(), new Document("bar", 1), Person.class, Jedi.class, + FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("bar", 1))); } @@ -367,7 +370,8 @@ public class ReactiveMongoTemplateUnitTests { @Test // DATAMONGO-1719 public void doesNotApplyFieldsWhenTargetIsNotAProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, Person.class, null).subscribe(); + template.doFind("star-wars", new Document(), new Document(), Person.class, Person.class, + FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } @@ -375,7 +379,8 @@ public class ReactiveMongoTemplateUnitTests { @Test // DATAMONGO-1719 public void doesNotApplyFieldsWhenTargetExtendsDomainType() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonExtended.class, null).subscribe(); + template.doFind("star-wars", new Document(), new Document(), Person.class, PersonExtended.class, + FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } @@ -804,6 +809,30 @@ public class ReactiveMongoTemplateUnitTests { Assertions.assertThat(ReflectionTestUtils.getField(template, "entityCallbacks")).isSameAs(callbacks); } + @Test // DATAMONGO-2344 + public void slaveOkQueryOptionShouldApplyPrimaryPreferredReadPreferenceForFind() { + + template.find(new Query().slaveOk(), AutogenerateableId.class).subscribe(); + + verify(collection).withReadPreference(eq(ReadPreference.primaryPreferred())); + } + + @Test // DATAMONGO-2344 + public void slaveOkQueryOptionShouldApplyPrimaryPreferredReadPreferenceForFindOne() { + + template.findOne(new Query().slaveOk(), AutogenerateableId.class).subscribe(); + + verify(collection).withReadPreference(eq(ReadPreference.primaryPreferred())); + } + + @Test // DATAMONGO-2344 + public void slaveOkQueryOptionShouldApplyPrimaryPreferredReadPreferenceForFindDistinct() { + + template.findDistinct(new Query().slaveOk(), "name", AutogenerateableId.class, String.class).subscribe(); + + verify(collection).withReadPreference(eq(ReadPreference.primaryPreferred())); + } + @Data @org.springframework.data.mongodb.core.mapping.Document(collection = "star-wars") static class Person {