Skip to content

Commit f774e35

Browse files
christophstroblmp911de
authored andcommitted
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.
1 parent 0a95fd9 commit f774e35

File tree

9 files changed

+394
-11
lines changed

9 files changed

+394
-11
lines changed

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

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,8 +1179,11 @@ <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions option
11791179
* {@literal null}.
11801180
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
11811181
* @return the count of matching documents.
1182+
* @since 3.4
11821183
*/
1183-
long count(Query query, Class<?> entityClass);
1184+
default long exactCount(Query query, Class<?> entityClass) {
1185+
return exactCount(query, entityClass, getCollectionName(entityClass));
1186+
}
11841187

11851188
/**
11861189
* Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query}
@@ -1201,6 +1204,71 @@ <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions option
12011204
* @param collectionName must not be {@literal null} or empty.
12021205
* @return the count of matching documents.
12031206
* @see #count(Query, Class, String)
1207+
* @since 3.4
1208+
*/
1209+
default long exactCount(Query query, String collectionName) {
1210+
return exactCount(query, null, collectionName);
1211+
}
1212+
1213+
/**
1214+
* Returns the number of documents for the given {@link Query} by querying the given collection using the given entity
1215+
* class to map the given {@link Query}. <br />
1216+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
1217+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
1218+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
1219+
* count all matches.
1220+
* <br />
1221+
* This method uses an
1222+
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1223+
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees
1224+
* shard, session and transaction compliance. In case an inaccurate count satisfies the applications needs use
1225+
* {@link #estimatedCount(String)} for empty queries instead.
1226+
*
1227+
* @param query the {@link Query} class that specifies the criteria used to find documents. Must not be
1228+
* {@literal null}.
1229+
* @param entityClass the parametrized type. Can be {@literal null}.
1230+
* @param collectionName must not be {@literal null} or empty.
1231+
* @return the count of matching documents.
1232+
* @since 3.4
1233+
*/
1234+
long exactCount(Query query, @Nullable Class<?> entityClass, String collectionName);
1235+
1236+
/**
1237+
* Returns the number of documents for the given {@link Query} by querying the collection of the given entity class.
1238+
* <br />
1239+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
1240+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
1241+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
1242+
* count all matches.
1243+
* <br />
1244+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
1245+
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1246+
* aggregation execution} which may have an impact on performance.
1247+
*
1248+
* @param query the {@link Query} class that specifies the criteria used to find documents. Must not be
1249+
* {@literal null}.
1250+
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
1251+
* @return the count of matching documents.
1252+
*/
1253+
long count(Query query, Class<?> entityClass);
1254+
1255+
/**
1256+
* Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query}
1257+
* must solely consist of document field references as we lack type information to map potential property references
1258+
* onto document fields. Use {@link #count(Query, Class, String)} to get full type specific support. <br />
1259+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
1260+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
1261+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
1262+
* count all matches.
1263+
* <br />
1264+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
1265+
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1266+
* aggregation execution} which may have an impact on performance.
1267+
*
1268+
* @param query the {@link Query} class that specifies the criteria used to find documents.
1269+
* @param collectionName must not be {@literal null} or empty.
1270+
* @return the count of matching documents.
1271+
* @see #count(Query, Class, String)
12041272
*/
12051273
long count(Query query, String collectionName);
12061274

@@ -1241,11 +1309,9 @@ default long estimatedCount(Class<?> entityClass) {
12411309
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
12421310
* count all matches.
12431311
* <br />
1244-
* This method uses an
1312+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
12451313
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1246-
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees
1247-
* shard, session and transaction compliance. In case an inaccurate count satisfies the applications needs use
1248-
* {@link #estimatedCount(String)} for empty queries instead.
1314+
* aggregation execution} which may have an impact on performance.
12491315
*
12501316
* @param query the {@link Query} class that specifies the criteria used to find documents. Must not be
12511317
* {@literal null}.

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

Lines changed: 84 additions & 1 deletion
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,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
188188

189189
private SessionSynchronization sessionSynchronization = SessionSynchronization.ON_ACTUAL_TRANSACTION;
190190

191+
private CountExecution countExecution = this::doExactCount;
192+
191193
/**
192194
* Constructor used for a basic template configuration.
193195
*
@@ -345,6 +347,47 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) {
345347
this.entityCallbacks = entityCallbacks;
346348
}
347349

350+
/**
351+
* En-/Disable usage of estimated count.
352+
*
353+
* @param enabled if {@literal true} {@link MongoCollection#estimatedDocumentCount()} ()} will we used for unpaged,
354+
* empty {@link Query queries}.
355+
* @since 3.4
356+
*/
357+
public void useEstimatedCount(boolean enabled) {
358+
useEstimatedCount(enabled, this::countCanBeEstimated);
359+
}
360+
361+
/**
362+
* En-/Disable usage of estimated count based on the given {@link BiPredicate estimationFilter}.
363+
*
364+
* @param enabled if {@literal true} {@link MongoCollection#estimatedDocumentCount()} will we used for {@link Document
365+
* filter queries} that pass the given {@link BiPredicate estimationFilter}.
366+
* @param estimationFilter the {@link BiPredicate filter}.
367+
* @since 3.4
368+
*/
369+
private void useEstimatedCount(boolean enabled, BiPredicate<Document, CountOptions> estimationFilter) {
370+
371+
if (enabled) {
372+
373+
this.countExecution = (collectionName, filter, options) -> {
374+
375+
if (!estimationFilter.test(filter, options)) {
376+
return doExactCount(collectionName, filter, options);
377+
}
378+
379+
EstimatedDocumentCountOptions estimatedDocumentCountOptions = new EstimatedDocumentCountOptions();
380+
if (options.getMaxTime(TimeUnit.MILLISECONDS) > 0) {
381+
estimatedDocumentCountOptions.maxTime(options.getMaxTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
382+
}
383+
384+
return doEstimatedCount(collectionName, estimatedDocumentCountOptions);
385+
};
386+
} else {
387+
this.countExecution = this::doExactCount;
388+
}
389+
}
390+
348391
/**
349392
* Inspects the given {@link ApplicationContext} for {@link MongoPersistentEntityIndexCreator} and those in turn if
350393
* they were registered for the current {@link MappingContext}. If no creator for the current {@link MappingContext}
@@ -1106,6 +1149,17 @@ public long count(Query query, String collectionName) {
11061149
return count(query, null, collectionName);
11071150
}
11081151

1152+
@Override
1153+
public long exactCount(Query query, @Nullable Class<?> entityClass, String collectionName) {
1154+
1155+
CountContext countContext = queryOperations.countQueryContext(query);
1156+
1157+
CountOptions options = countContext.getCountOptions(entityClass);
1158+
Document mappedQuery = countContext.getMappedQuery(entityClass, mappingContext::getPersistentEntity);
1159+
1160+
return doExactCount(collectionName, mappedQuery, options);
1161+
}
1162+
11091163
/*
11101164
* (non-Javadoc)
11111165
* @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 @@ protected long doCount(String collectionName, Document filter, CountOptions opti
11311185
.debug(String.format("Executing count: %s in collection: %s", serializeToJsonSafely(filter), collectionName));
11321186
}
11331187

1188+
return countExecution.countDocuments(collectionName, filter, options);
1189+
}
1190+
1191+
protected long doExactCount(String collectionName, Document filter, CountOptions options) {
11341192
return execute(collectionName,
11351193
collection -> collection.countDocuments(CountQuery.of(filter).toQueryDocument(), options));
11361194
}
11371195

1196+
protected boolean countCanBeEstimated(Document filter, CountOptions options) {
1197+
1198+
return
1199+
// only empty filter for estimatedCount
1200+
filter.isEmpty() &&
1201+
// no skip, no limit,...
1202+
isEmptyOptions(options) &&
1203+
// transaction active?
1204+
!MongoDatabaseUtils.isTransactionActive(getMongoDatabaseFactory());
1205+
}
1206+
1207+
private boolean isEmptyOptions(CountOptions options) {
1208+
return options.getLimit() <= 0 && options.getSkip() <= 0;
1209+
}
1210+
11381211
/*
11391212
* (non-Javadoc)
11401213
* @see org.springframework.data.mongodb.core.MongoOperations#estimatedCount(java.lang.String)
@@ -3571,5 +3644,15 @@ public MongoDatabase getDb() {
35713644
// native MongoDB objects that offer methods with ClientSession must not be proxied.
35723645
return delegate.getDb();
35733646
}
3647+
3648+
@Override
3649+
protected boolean countCanBeEstimated(Document filter, CountOptions options) {
3650+
return false;
3651+
}
3652+
}
3653+
3654+
@FunctionalInterface
3655+
interface CountExecution {
3656+
long countDocuments(String collection, Document filter, CountOptions options);
35743657
}
35753658
}

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

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -951,8 +951,11 @@ <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions
951951
* {@literal null}.
952952
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
953953
* @return the count of matching documents.
954+
* @since 3.4
954955
*/
955-
Mono<Long> count(Query query, Class<?> entityClass);
956+
default Mono<Long> exactCount(Query query, Class<?> entityClass) {
957+
return exactCount(query, entityClass, getCollectionName(entityClass));
958+
}
956959

957960
/**
958961
* Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query}
@@ -973,8 +976,11 @@ <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions
973976
* @param collectionName must not be {@literal null} or empty.
974977
* @return the count of matching documents.
975978
* @see #count(Query, Class, String)
979+
* @since 3.4
976980
*/
977-
Mono<Long> count(Query query, String collectionName);
981+
default Mono<Long> exactCount(Query query, String collectionName) {
982+
return exactCount(query, null, collectionName);
983+
}
978984

979985
/**
980986
* Returns the number of documents for the given {@link Query} by querying the given collection using the given entity
@@ -995,6 +1001,66 @@ <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions
9951001
* @param entityClass the parametrized type. Can be {@literal null}.
9961002
* @param collectionName must not be {@literal null} or empty.
9971003
* @return the count of matching documents.
1004+
* @since 3.4
1005+
*/
1006+
Mono<Long> exactCount(Query query, @Nullable Class<?> entityClass, String collectionName);
1007+
1008+
/**
1009+
* Returns the number of documents for the given {@link Query} by querying the collection of the given entity class.
1010+
* <br />
1011+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
1012+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
1013+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
1014+
* count all matches.
1015+
* <br />
1016+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
1017+
* {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1018+
* aggregation execution} which may have an impact on performance.
1019+
*
1020+
* @param query the {@link Query} class that specifies the criteria used to find documents. Must not be
1021+
* {@literal null}.
1022+
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
1023+
* @return the count of matching documents.
1024+
*/
1025+
Mono<Long> count(Query query, Class<?> entityClass);
1026+
1027+
/**
1028+
* Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query}
1029+
* must solely consist of document field references as we lack type information to map potential property references
1030+
* onto document fields. Use {@link #count(Query, Class, String)} to get full type specific support. <br />
1031+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
1032+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
1033+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
1034+
* count all matches.
1035+
* <br />
1036+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
1037+
* {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1038+
* aggregation execution} which may have an impact on performance.
1039+
*
1040+
* @param query the {@link Query} class that specifies the criteria used to find documents.
1041+
* @param collectionName must not be {@literal null} or empty.
1042+
* @return the count of matching documents.
1043+
* @see #count(Query, Class, String)
1044+
*/
1045+
Mono<Long> count(Query query, String collectionName);
1046+
1047+
/**
1048+
* Returns the number of documents for the given {@link Query} by querying the given collection using the given entity
1049+
* class to map the given {@link Query}. <br />
1050+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
1051+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
1052+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
1053+
* count all matches.
1054+
* <br />
1055+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
1056+
* {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1057+
* aggregation execution} which may have an impact on performance.
1058+
*
1059+
* @param query the {@link Query} class that specifies the criteria used to find documents. Must not be
1060+
* {@literal null}.
1061+
* @param entityClass the parametrized type. Can be {@literal null}.
1062+
* @param collectionName must not be {@literal null} or empty.
1063+
* @return the count of matching documents.
9981064
*/
9991065
Mono<Long> count(Query query, @Nullable Class<?> entityClass, String collectionName);
10001066

0 commit comments

Comments
 (0)