Skip to content

Commit 002bda7

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. To still be able to retrieve the precise number of matching documents we also introduced MongoTemplate#preciseCount.
1 parent 45dfdf7 commit 002bda7

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
@@ -1171,8 +1171,11 @@ <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions option
11711171
* {@literal null}.
11721172
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
11731173
* @return the count of matching documents.
1174+
* @since 3.4
11741175
*/
1175-
long count(Query query, Class<?> entityClass);
1176+
default long preciseCount(Query query, Class<?> entityClass) {
1177+
return preciseCount(query, entityClass, getCollectionName(entityClass));
1178+
}
11761179

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

@@ -1233,11 +1301,9 @@ default long estimatedCount(Class<?> entityClass) {
12331301
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
12341302
* count all matches.
12351303
* <br />
1236-
* This method uses an
1304+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
12371305
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1238-
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees
1239-
* shard, session and transaction compliance. In case an inaccurate count satisfies the applications needs use
1240-
* {@link #estimatedCount(String)} for empty queries instead.
1306+
* aggregation execution} which may have an impact on performance.
12411307
*
12421308
* @param query the {@link Query} class that specifies the criteria used to find documents. Must not be
12431309
* {@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::doPreciseCount;
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+
public 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 doPreciseCount(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::doPreciseCount;
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 preciseCount(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 doPreciseCount(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 doPreciseCount(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)
@@ -3569,5 +3642,15 @@ public MongoDatabase getDb() {
35693642
// native MongoDB objects that offer methods with ClientSession must not be proxied.
35703643
return delegate.getDb();
35713644
}
3645+
3646+
@Override
3647+
protected boolean countCanBeEstimated(Document filter, CountOptions options) {
3648+
return false;
3649+
}
3650+
}
3651+
3652+
@FunctionalInterface
3653+
interface CountExecution {
3654+
long countDocuments(String collection, Document filter, CountOptions options);
35723655
}
35733656
}

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> preciseCount(Query query, Class<?> entityClass) {
957+
return preciseCount(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> preciseCount(Query query, String collectionName) {
982+
return preciseCount(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> preciseCount(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)