From f774e35af2fb44c5698485b39109f638a1a2491b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 27 Jan 2022 12:30:34 +0100 Subject: [PATCH] Allow to estimate document count. This commit introduce an option that allows users to opt in on using estimatedDocumentCount instead of countDocuments in case the used filter query is empty. To still be able to retrieve the exact number of matching documents we also introduced MongoTemplate#exactCount. Closes: #3522 Original pull request: #3951. --- .../data/mongodb/core/MongoOperations.java | 76 ++++++++++++++-- .../data/mongodb/core/MongoTemplate.java | 85 +++++++++++++++++- .../mongodb/core/ReactiveMongoOperations.java | 70 ++++++++++++++- .../mongodb/core/ReactiveMongoTemplate.java | 90 ++++++++++++++++++- .../mongodb/core/MongoTemplateUnitTests.java | 28 ++++++ .../core/ReactiveMongoTemplateUnitTests.java | 28 ++++++ ...iveSessionBoundMongoTemplateUnitTests.java | 11 ++- .../SessionBoundMongoTemplateUnitTests.java | 11 ++- src/main/asciidoc/reference/mongodb.adoc | 6 ++ 9 files changed, 394 insertions(+), 11 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index fd014c2d3..0e5feea19 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -1179,8 +1179,11 @@ public interface MongoOperations extends FluentMongoOperations { * {@literal null}. * @param entityClass class that determines the collection to use. Must not be {@literal null}. * @return the count of matching documents. + * @since 3.4 */ - long count(Query query, Class entityClass); + default long exactCount(Query query, Class entityClass) { + return exactCount(query, entityClass, getCollectionName(entityClass)); + } /** * Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query} @@ -1201,6 +1204,71 @@ public interface MongoOperations extends FluentMongoOperations { * @param collectionName must not be {@literal null} or empty. * @return the count of matching documents. * @see #count(Query, Class, String) + * @since 3.4 + */ + default long exactCount(Query query, String collectionName) { + return exactCount(query, null, collectionName); + } + + /** + * Returns the number of documents for the given {@link Query} by querying the given collection using the given entity + * class to map the given {@link Query}.
+ * NOTE: Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct + * influence on the resulting number of documents found as those values are passed on to the server and potentially + * limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to + * count all matches. + *
+ * This method uses an + * {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions) + * aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees + * shard, session and transaction compliance. In case an inaccurate count satisfies the applications needs use + * {@link #estimatedCount(String)} for empty queries instead. + * + * @param query the {@link Query} class that specifies the criteria used to find documents. Must not be + * {@literal null}. + * @param entityClass the parametrized type. Can be {@literal null}. + * @param collectionName must not be {@literal null} or empty. + * @return the count of matching documents. + * @since 3.4 + */ + long exactCount(Query query, @Nullable Class entityClass, String collectionName); + + /** + * Returns the number of documents for the given {@link Query} by querying the collection of the given entity class. + *
+ * NOTE: Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct + * influence on the resulting number of documents found as those values are passed on to the server and potentially + * limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to + * count all matches. + *
+ * This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an + * {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions) + * aggregation execution} which may have an impact on performance. + * + * @param query the {@link Query} class that specifies the criteria used to find documents. Must not be + * {@literal null}. + * @param entityClass class that determines the collection to use. Must not be {@literal null}. + * @return the count of matching documents. + */ + long count(Query query, Class entityClass); + + /** + * Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query} + * must solely consist of document field references as we lack type information to map potential property references + * onto document fields. Use {@link #count(Query, Class, String)} to get full type specific support.
+ * NOTE: Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct + * influence on the resulting number of documents found as those values are passed on to the server and potentially + * limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to + * count all matches. + *
+ * This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an + * {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions) + * aggregation execution} which may have an impact on performance. + * + * @param query the {@link Query} class that specifies the criteria used to find documents. + * @param collectionName must not be {@literal null} or empty. + * @return the count of matching documents. + * @see #count(Query, Class, String) */ long count(Query query, String collectionName); @@ -1241,11 +1309,9 @@ public interface MongoOperations extends FluentMongoOperations { * limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to * count all matches. *
- * This method uses an + * This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an * {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions) - * aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees - * shard, session and transaction compliance. In case an inaccurate count satisfies the applications needs use - * {@link #estimatedCount(String)} for empty queries instead. + * aggregation execution} which may have an impact on performance. * * @param query the {@link Query} class that specifies the criteria used to find documents. Must not be * {@literal 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 3d159c6e9..b8bac461d 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 @@ -22,13 +22,13 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.function.BiPredicate; import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.Document; import org.bson.conversions.Bson; - import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -188,6 +188,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private SessionSynchronization sessionSynchronization = SessionSynchronization.ON_ACTUAL_TRANSACTION; + private CountExecution countExecution = this::doExactCount; + /** * Constructor used for a basic template configuration. * @@ -345,6 +347,47 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.entityCallbacks = entityCallbacks; } + /** + * En-/Disable usage of estimated count. + * + * @param enabled if {@literal true} {@link MongoCollection#estimatedDocumentCount()} ()} will we used for unpaged, + * empty {@link Query queries}. + * @since 3.4 + */ + public void useEstimatedCount(boolean enabled) { + useEstimatedCount(enabled, this::countCanBeEstimated); + } + + /** + * En-/Disable usage of estimated count based on the given {@link BiPredicate estimationFilter}. + * + * @param enabled if {@literal true} {@link MongoCollection#estimatedDocumentCount()} will we used for {@link Document + * filter queries} that pass the given {@link BiPredicate estimationFilter}. + * @param estimationFilter the {@link BiPredicate filter}. + * @since 3.4 + */ + private void useEstimatedCount(boolean enabled, BiPredicate estimationFilter) { + + if (enabled) { + + this.countExecution = (collectionName, filter, options) -> { + + if (!estimationFilter.test(filter, options)) { + return doExactCount(collectionName, filter, options); + } + + EstimatedDocumentCountOptions estimatedDocumentCountOptions = new EstimatedDocumentCountOptions(); + if (options.getMaxTime(TimeUnit.MILLISECONDS) > 0) { + estimatedDocumentCountOptions.maxTime(options.getMaxTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); + } + + return doEstimatedCount(collectionName, estimatedDocumentCountOptions); + }; + } else { + this.countExecution = this::doExactCount; + } + } + /** * Inspects the given {@link ApplicationContext} for {@link MongoPersistentEntityIndexCreator} and those in turn if * they were registered for the current {@link MappingContext}. If no creator for the current {@link MappingContext} @@ -1106,6 +1149,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return count(query, null, collectionName); } + @Override + public long exactCount(Query query, @Nullable Class entityClass, String collectionName) { + + CountContext countContext = queryOperations.countQueryContext(query); + + CountOptions options = countContext.getCountOptions(entityClass); + Document mappedQuery = countContext.getMappedQuery(entityClass, mappingContext::getPersistentEntity); + + return doExactCount(collectionName, mappedQuery, options); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.MongoOperations#count(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String) @@ -1131,10 +1185,29 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, .debug(String.format("Executing count: %s in collection: %s", serializeToJsonSafely(filter), collectionName)); } + return countExecution.countDocuments(collectionName, filter, options); + } + + protected long doExactCount(String collectionName, Document filter, CountOptions options) { return execute(collectionName, collection -> collection.countDocuments(CountQuery.of(filter).toQueryDocument(), options)); } + protected boolean countCanBeEstimated(Document filter, CountOptions options) { + + return + // only empty filter for estimatedCount + filter.isEmpty() && + // no skip, no limit,... + isEmptyOptions(options) && + // transaction active? + !MongoDatabaseUtils.isTransactionActive(getMongoDatabaseFactory()); + } + + private boolean isEmptyOptions(CountOptions options) { + return options.getLimit() <= 0 && options.getSkip() <= 0; + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.MongoOperations#estimatedCount(java.lang.String) @@ -3571,5 +3644,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, // native MongoDB objects that offer methods with ClientSession must not be proxied. return delegate.getDb(); } + + @Override + protected boolean countCanBeEstimated(Document filter, CountOptions options) { + return false; + } + } + + @FunctionalInterface + interface CountExecution { + long countDocuments(String collection, Document filter, CountOptions options); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java index 31231ca7e..d50355dac 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java @@ -951,8 +951,11 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * {@literal null}. * @param entityClass class that determines the collection to use. Must not be {@literal null}. * @return the count of matching documents. + * @since 3.4 */ - Mono count(Query query, Class entityClass); + default Mono exactCount(Query query, Class entityClass) { + return exactCount(query, entityClass, getCollectionName(entityClass)); + } /** * Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query} @@ -973,8 +976,11 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * @param collectionName must not be {@literal null} or empty. * @return the count of matching documents. * @see #count(Query, Class, String) + * @since 3.4 */ - Mono count(Query query, String collectionName); + default Mono exactCount(Query query, String collectionName) { + return exactCount(query, null, collectionName); + } /** * Returns the number of documents for the given {@link Query} by querying the given collection using the given entity @@ -995,6 +1001,66 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { * @param entityClass the parametrized type. Can be {@literal null}. * @param collectionName must not be {@literal null} or empty. * @return the count of matching documents. + * @since 3.4 + */ + Mono exactCount(Query query, @Nullable Class entityClass, String collectionName); + + /** + * Returns the number of documents for the given {@link Query} by querying the collection of the given entity class. + *
+ * NOTE: Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct + * influence on the resulting number of documents found as those values are passed on to the server and potentially + * limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to + * count all matches. + *
+ * This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an + * {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions) + * aggregation execution} which may have an impact on performance. + * + * @param query the {@link Query} class that specifies the criteria used to find documents. Must not be + * {@literal null}. + * @param entityClass class that determines the collection to use. Must not be {@literal null}. + * @return the count of matching documents. + */ + Mono count(Query query, Class entityClass); + + /** + * Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query} + * must solely consist of document field references as we lack type information to map potential property references + * onto document fields. Use {@link #count(Query, Class, String)} to get full type specific support.
+ * NOTE: Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct + * influence on the resulting number of documents found as those values are passed on to the server and potentially + * limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to + * count all matches. + *
+ * This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an + * {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions) + * aggregation execution} which may have an impact on performance. + * + * @param query the {@link Query} class that specifies the criteria used to find documents. + * @param collectionName must not be {@literal null} or empty. + * @return the count of matching documents. + * @see #count(Query, Class, String) + */ + Mono count(Query query, String collectionName); + + /** + * Returns the number of documents for the given {@link Query} by querying the given collection using the given entity + * class to map the given {@link Query}.
+ * NOTE: Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct + * influence on the resulting number of documents found as those values are passed on to the server and potentially + * limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to + * count all matches. + *
+ * This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an + * {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions) + * aggregation execution} which may have an impact on performance. + * + * @param query the {@link Query} class that specifies the criteria used to find documents. Must not be + * {@literal null}. + * @param entityClass the parametrized type. Can be {@literal null}. + * @param collectionName must not be {@literal null} or empty. + * @return the count of matching documents. */ Mono count(Query query, @Nullable Class entityClass, String collectionName); 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 a8a19722d..49c6e075c 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 @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -190,6 +191,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private SessionSynchronization sessionSynchronization = SessionSynchronization.ON_ACTUAL_TRANSACTION; + private CountExecution countExecution = this::doExactCount; + /** * Constructor used for a basic template configuration. * @@ -369,6 +372,49 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.entityCallbacks = entityCallbacks; } + /** + * En-/Disable usage of estimated count. + * + * @param enabled if {@literal true} {@link com.mongodb.client.MongoCollection#estimatedDocumentCount()} ()} will we used for unpaged, + * empty {@link Query queries}. + * @since 3.4 + */ + public void useEstimatedCount(boolean enabled) { + useEstimatedCount(enabled, this::countCanBeEstimated); + } + + /** + * En-/Disable usage of estimated count based on the given {@link BiFunction estimationFilter}. + * + * @param enabled if {@literal true} {@link com.mongodb.client.MongoCollection#estimatedDocumentCount()} will we used for {@link Document + * filter queries} that pass the given {@link BiFunction estimationFilter}. + * @param estimationFilter the {@link BiFunction filter}. + * @since 3.4 + */ + private void useEstimatedCount(boolean enabled, BiFunction> estimationFilter) { + + if (enabled) { + + this.countExecution = (collectionName, filter, options) -> { + + return estimationFilter.apply(filter, options).flatMap(canEstimate -> { + if (!canEstimate) { + return doExactCount(collectionName, filter, options); + } + + EstimatedDocumentCountOptions estimatedDocumentCountOptions = new EstimatedDocumentCountOptions(); + if (options.getMaxTime(TimeUnit.MILLISECONDS) > 0) { + estimatedDocumentCountOptions.maxTime(options.getMaxTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); + } + + return doEstimatedCount(collectionName, estimatedDocumentCountOptions); + }); + }; + } else { + this.countExecution = this::doExactCount; + } + } + /** * Inspects the given {@link ApplicationContext} for {@link ReactiveMongoPersistentEntityIndexCreator} and those in * turn if they were registered for the current {@link MappingContext}. If no creator for the current @@ -1189,6 +1235,17 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati entityClass); } + @Override + public Mono exactCount(Query query, @Nullable Class entityClass, String collectionName) { + + CountContext countContext = queryOperations.countQueryContext(query); + + CountOptions options = countContext.getCountOptions(entityClass); + Document mappedQuery = countContext.getMappedQuery(entityClass, mappingContext::getPersistentEntity); + + return doExactCount(collectionName, mappedQuery, options); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#count(org.springframework.data.mongodb.core.query.Query, java.lang.Class) @@ -1252,15 +1309,36 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati */ protected Mono doCount(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); + } + + protected Mono doExactCount(String collectionName, Document filter, CountOptions options) { + return createMono(collectionName, collection -> collection.countDocuments(CountQuery.of(filter).toQueryDocument(), options)); } protected Mono doEstimatedCount(String collectionName, EstimatedDocumentCountOptions options) { - return createMono(collectionName, collection -> collection.estimatedDocumentCount(options)); } + protected Mono countCanBeEstimated(Document filter, CountOptions options) { + + if(!filter.isEmpty() || !isEmptyOptions(options)) { + return Mono.just(false); + } + return ReactiveMongoDatabaseUtils.isTransactionActive(getMongoDatabaseFactory()).map(it -> !it); + } + + private boolean isEmptyOptions(CountOptions options) { + return options.getLimit() <= 0 && options.getSkip() <= 0; + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#insert(reactor.core.publisher.Mono) @@ -3444,6 +3522,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati // native MongoDB objects that offer methods with ClientSession must not be proxied. return delegate.getMongoDatabase(); } + + @Override + protected Mono countCanBeEstimated(Document filter, CountOptions options) { + return Mono.just(false); + } } class IndexCreatorEventListener implements ApplicationListener> { @@ -3521,4 +3604,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return collection; } } + + @FunctionalInterface + interface CountExecution { + Mono countDocuments(String collection, Document filter, CountOptions options); + } } 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 f5953d038..5eed639b6 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 @@ -2289,6 +2289,34 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { .granularity(TimeSeriesGranularity.HOURS).toString()); } + @Test // GH-3522 + void usedCountDocumentsForEmptyQueryByDefault() { + + template.count(new Query(), Human.class); + + verify(collection).countDocuments(any(Document.class), any()); + } + + @Test // GH-3522 + void delegatesToEstimatedCountForEmptyQueryIfEnabled() { + + template.useEstimatedCount(true); + + template.count(new Query(), Human.class); + + verify(collection).estimatedDocumentCount(any()); + } + + @Test // GH-3522 + void stillUsesCountDocumentsForNonEmptyQueryEvenIfEstimationEnabled() { + + template.useEstimatedCount(true); + + template.count(new BasicQuery("{ 'spring' : 'data-mongodb' }"), Human.class); + + verify(collection).countDocuments(any(Document.class), any()); + } + 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 55b8edafb..0d9bca468 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 @@ -1424,6 +1424,34 @@ public class ReactiveMongoTemplateUnitTests { verify(collection).estimatedDocumentCount(any()); } + @Test // GH-3522 + void usedCountDocumentsForEmptyQueryByDefault() { + + template.count(new Query(), Person.class).subscribe(); + + verify(collection).countDocuments(any(Document.class), any()); + } + + @Test // GH-3522 + void delegatesToEstimatedCountForEmptyQueryIfEnabled() { + + template.useEstimatedCount(true); + + template.count(new Query(), Person.class).subscribe(); + + verify(collection).estimatedDocumentCount(any()); + } + + @Test // GH-3522 + void stillUsesCountDocumentsForNonEmptyQueryEvenIfEstimationEnabled() { + + template.useEstimatedCount(true); + + template.count(new BasicQuery("{ 'spring' : 'data-mongodb' }"), Person.class).subscribe(); + + verify(collection).countDocuments(any(Document.class), any()); + } + @Test // GH-2911 void insertErrorsOnPublisher() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java index 8efee4e21..2384340e9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java @@ -220,7 +220,7 @@ public class ReactiveSessionBoundMongoTemplateUnitTests { verify(database).listCollectionNames(eq(clientSession)); } - @Test // DATAMONGO-1880 + @Test // DATAMONGO-1880, GH-3522 public void countShouldUseProxiedCollection() { template.count(new Query(), Person.class).subscribe(); @@ -228,6 +228,15 @@ public class ReactiveSessionBoundMongoTemplateUnitTests { verify(collection).countDocuments(eq(clientSession), any(), any(CountOptions.class)); } + @Test // GH-3522 + public void countShouldDelegateToExactCountNoMatterWhat() { + + template.useEstimatedCount(true); + template.count(new Query(), Person.class).subscribe(); + + verify(collection).countDocuments(eq(clientSession), any(), any(CountOptions.class)); + } + @Test // DATAMONGO-1880 public void createCollectionShouldUseProxiedDatabase() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java index c6697584f..901b9b82a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java @@ -220,7 +220,7 @@ public class SessionBoundMongoTemplateUnitTests { verify(database).listCollectionNames(eq(clientSession)); } - @Test // DATAMONGO-1880 + @Test // DATAMONGO-1880, GH-3522 public void countShouldUseProxiedCollection() { template.count(new Query(), Person.class); @@ -228,6 +228,15 @@ public class SessionBoundMongoTemplateUnitTests { verify(collection).countDocuments(eq(clientSession), any(), any(CountOptions.class)); } + @Test // DATAMONGO-1880, GH-3522 + public void countShouldDelegateToExactCountNoMatterWhat() { + + template.useEstimatedCount(true); + template.count(new Query(), Person.class); + + verify(collection).countDocuments(eq(clientSession), any(), any(CountOptions.class)); + } + @Test // DATAMONGO-1880 public void createCollectionShouldUseProxiedDatabase() { diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 98aeaa8e9..a8caa7b0e 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -2158,6 +2158,12 @@ So in version 2.x `MongoOperations.count()` would use the collection statistics As of Spring Data MongoDB 3.x any `count` operation uses regardless the existence of filter criteria the aggregation-based count approach via MongoDBs `countDocuments`. If the application is fine with the limitations of working upon collection statistics `MongoOperations.estimatedCount()` offers an alternative. +[TIP] +==== +By setting `MongoTemplate#useEstimatedCount(...)` to `true` _MongoTemplate#count(...)_ operations, that use an empty filter query, will be delegated to `estimatedCount`, as long as there is no transaction active and the template is not bound to a <>. +It will still be possible to obtain exact numbers via `MongoTemplate#exactCount`, but may speed up things. +==== + [NOTE] ==== MongoDBs native `countDocuments` method and the `$match` aggregation, do not support `$near` and `$nearSphere` but require `$geoWithin` along with `$center` or `$centerSphere` which does not support `$minDistance` (see https://jira.mongodb.org/browse/SERVER-37043).