Skip to content

Commit 360c223

Browse files
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.
1 parent 29bd766 commit 360c223

File tree

4 files changed

+111
-4
lines changed

4 files changed

+111
-4
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
import java.math.RoundingMode;
2323
import java.util.*;
2424
import java.util.concurrent.TimeUnit;
25+
import java.util.function.BiPredicate;
2526
import java.util.stream.Collectors;
2627

2728
import org.apache.commons.logging.Log;
2829
import org.apache.commons.logging.LogFactory;
2930
import org.bson.Document;
3031
import org.bson.conversions.Bson;
31-
3232
import org.springframework.beans.BeansException;
3333
import org.springframework.context.ApplicationContext;
3434
import org.springframework.context.ApplicationContextAware;
@@ -188,6 +188,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
188188

189189
private SessionSynchronization sessionSynchronization = SessionSynchronization.ON_ACTUAL_TRANSACTION;
190190

191+
private CountExecution countExecution = (collectionName, filter, options) -> {
192+
return execute(collectionName,
193+
collection -> collection.countDocuments(CountQuery.of(filter).toQueryDocument(), options));
194+
};
195+
191196
/**
192197
* Constructor used for a basic template configuration.
193198
*
@@ -345,6 +350,51 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) {
345350
this.entityCallbacks = entityCallbacks;
346351
}
347352

353+
/**
354+
* En-/Disable usage of estimated count.
355+
*
356+
* @param enabled if {@literal true} {@link MongoCollection#estimatedDocumentCount()} ()} will we used for unpaged,
357+
* empty {@link Query queries}.
358+
* @since 3.4
359+
*/
360+
public void useEstimatedCount(boolean enabled) {
361+
useEstimatedCount(enabled, this::countCanBeEstimated);
362+
}
363+
364+
/**
365+
* En-/Disable usage of estimated count based on the given {@link BiPredicate estimationFilter}.
366+
*
367+
* @param enabled if {@literal true} {@link MongoCollection#estimatedDocumentCount()} will we used for {@link Document
368+
* filter queries} that pass the given {@link BiPredicate estimationFilter}.
369+
* @param estimationFilter the {@link BiPredicate filter}.
370+
* @since 3.4
371+
*/
372+
public void useEstimatedCount(boolean enabled, BiPredicate<Document, CountOptions> estimationFilter) {
373+
374+
if (enabled) {
375+
376+
this.countExecution = (collectionName, filter, options) -> {
377+
378+
if (!estimationFilter.test(filter, options)) {
379+
return execute(collectionName,
380+
collection -> collection.countDocuments(CountQuery.of(filter).toQueryDocument(), options));
381+
}
382+
383+
EstimatedDocumentCountOptions estimatedDocumentCountOptions = new EstimatedDocumentCountOptions();
384+
if (options.getMaxTime(TimeUnit.MILLISECONDS) > 0) {
385+
estimatedDocumentCountOptions.maxTime(options.getMaxTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
386+
}
387+
388+
return doEstimatedCount(collectionName, estimatedDocumentCountOptions);
389+
};
390+
} else {
391+
this.countExecution = (collectionName, filter, options) -> {
392+
return execute(collectionName,
393+
collection -> collection.countDocuments(CountQuery.of(filter).toQueryDocument(), options));
394+
};
395+
}
396+
}
397+
348398
/**
349399
* Inspects the given {@link ApplicationContext} for {@link MongoPersistentEntityIndexCreator} and those in turn if
350400
* they were registered for the current {@link MappingContext}. If no creator for the current {@link MappingContext}
@@ -1131,8 +1181,22 @@ protected long doCount(String collectionName, Document filter, CountOptions opti
11311181
.debug(String.format("Executing count: %s in collection: %s", serializeToJsonSafely(filter), collectionName));
11321182
}
11331183

1134-
return execute(collectionName,
1135-
collection -> collection.countDocuments(CountQuery.of(filter).toQueryDocument(), options));
1184+
return countExecution.countDocuments(collectionName, filter, options);
1185+
}
1186+
1187+
protected boolean countCanBeEstimated(Document filter, CountOptions options) {
1188+
1189+
return
1190+
// only empty filter for estimatedCount
1191+
filter.isEmpty() &&
1192+
// no skip, no limit,...
1193+
isEmptyOptions(options) &&
1194+
// transaction active?
1195+
!MongoDatabaseUtils.isTransactionActive(getMongoDatabaseFactory());
1196+
}
1197+
1198+
private boolean isEmptyOptions(CountOptions options) {
1199+
return options.getLimit() <= 0 && options.getSkip() <= 0;
11361200
}
11371201

11381202
/*
@@ -3569,5 +3633,15 @@ public MongoDatabase getDb() {
35693633
// native MongoDB objects that offer methods with ClientSession must not be proxied.
35703634
return delegate.getDb();
35713635
}
3636+
3637+
@Override
3638+
protected boolean countCanBeEstimated(Document filter, CountOptions options) {
3639+
return false;
3640+
}
3641+
}
3642+
3643+
@FunctionalInterface
3644+
interface CountExecution {
3645+
long countDocuments(String collection, Document filter, CountOptions options);
35723646
}
35733647
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2289,6 +2289,34 @@ void createCollectionShouldSetUpTimeSeries() {
22892289
.granularity(TimeSeriesGranularity.HOURS).toString());
22902290
}
22912291

2292+
@Test // GH-3522
2293+
void usedCountDocumentsForEmptyQueryByDefault() {
2294+
2295+
template.count(new Query(), Human.class);
2296+
2297+
verify(collection).countDocuments(any(Document.class), any());
2298+
}
2299+
2300+
@Test // GH-3522
2301+
void delegatesToEstimatedCountForEmptyQueryIfEnabled() {
2302+
2303+
template.useEstimatedCount(true);
2304+
2305+
template.count(new Query(), Human.class);
2306+
2307+
verify(collection).estimatedDocumentCount(any());
2308+
}
2309+
2310+
@Test // GH-3522
2311+
void stillUsesCountDocumentsForNonEmptyQueryEvenIfEstimationEnabled() {
2312+
2313+
template.useEstimatedCount(true);
2314+
2315+
template.count(new BasicQuery("{ 'spring' : 'data-mongodb' }"), Human.class);
2316+
2317+
verify(collection).countDocuments(any(Document.class), any());
2318+
}
2319+
22922320
class AutogenerateableId {
22932321

22942322
@Id BigInteger id;

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public void collectionExistsShouldUseProxiedDatabase() {
220220
verify(database).listCollectionNames(eq(clientSession));
221221
}
222222

223-
@Test // DATAMONGO-1880
223+
@Test // DATAMONGO-1880, GH-3522
224224
public void countShouldUseProxiedCollection() {
225225

226226
template.count(new Query(), Person.class);

src/main/asciidoc/reference/mongodb.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2152,6 +2152,11 @@ So in version 2.x `MongoOperations.count()` would use the collection statistics
21522152
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`.
21532153
If the application is fine with the limitations of working upon collection statistics `MongoOperations.estimatedCount()` offers an alternative.
21542154

2155+
[TIP]
2156+
====
2157+
By setting `MongoTemplate#useEstimatedCount(...)` to `true` _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 <<mongo.sessions,session>>.
2158+
====
2159+
21552160
[NOTE]
21562161
====
21572162
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).

0 commit comments

Comments
 (0)