Skip to content

Commit d16013a

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 dab5473 commit d16013a

File tree

9 files changed

+410
-10
lines changed

9 files changed

+410
-10
lines changed

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

+71-5
Original file line numberDiff line numberDiff line change
@@ -1144,8 +1144,11 @@ <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions option
11441144
* {@literal null}.
11451145
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
11461146
* @return the count of matching documents.
1147+
* @since 3.4
11471148
*/
1148-
long count(Query query, Class<?> entityClass);
1149+
default long exactCount(Query query, Class<?> entityClass) {
1150+
return exactCount(query, entityClass, getCollectionName(entityClass));
1151+
}
11491152

11501153
/**
11511154
* Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query}
@@ -1166,6 +1169,71 @@ <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions option
11661169
* @param collectionName must not be {@literal null} or empty.
11671170
* @return the count of matching documents.
11681171
* @see #count(Query, Class, String)
1172+
* @since 3.4
1173+
*/
1174+
default long exactCount(Query query, String collectionName) {
1175+
return exactCount(query, null, collectionName);
1176+
}
1177+
1178+
/**
1179+
* Returns the number of documents for the given {@link Query} by querying the given collection using the given entity
1180+
* class to map the given {@link Query}. <br />
1181+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
1182+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
1183+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
1184+
* count all matches.
1185+
* <br />
1186+
* This method uses an
1187+
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1188+
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees
1189+
* shard, session and transaction compliance. In case an inaccurate count satisfies the applications needs use
1190+
* {@link #estimatedCount(String)} for empty queries instead.
1191+
*
1192+
* @param query the {@link Query} class that specifies the criteria used to find documents. Must not be
1193+
* {@literal null}.
1194+
* @param entityClass the parametrized type. Can be {@literal null}.
1195+
* @param collectionName must not be {@literal null} or empty.
1196+
* @return the count of matching documents.
1197+
* @since 3.4
1198+
*/
1199+
long exactCount(Query query, @Nullable Class<?> entityClass, String collectionName);
1200+
1201+
/**
1202+
* Returns the number of documents for the given {@link Query} by querying the collection of the given entity class.
1203+
* <br />
1204+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
1205+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
1206+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
1207+
* count all matches.
1208+
* <br />
1209+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
1210+
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1211+
* aggregation execution} which may have an impact on performance.
1212+
*
1213+
* @param query the {@link Query} class that specifies the criteria used to find documents. Must not be
1214+
* {@literal null}.
1215+
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
1216+
* @return the count of matching documents.
1217+
*/
1218+
long count(Query query, Class<?> entityClass);
1219+
1220+
/**
1221+
* Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query}
1222+
* must solely consist of document field references as we lack type information to map potential property references
1223+
* onto document fields. Use {@link #count(Query, Class, String)} to get full type specific support. <br />
1224+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
1225+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
1226+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
1227+
* count all matches.
1228+
* <br />
1229+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
1230+
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1231+
* aggregation execution} which may have an impact on performance.
1232+
*
1233+
* @param query the {@link Query} class that specifies the criteria used to find documents.
1234+
* @param collectionName must not be {@literal null} or empty.
1235+
* @return the count of matching documents.
1236+
* @see #count(Query, Class, String)
11691237
*/
11701238
long count(Query query, String collectionName);
11711239

@@ -1206,11 +1274,9 @@ default long estimatedCount(Class<?> entityClass) {
12061274
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
12071275
* count all matches.
12081276
* <br />
1209-
* This method uses an
1277+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
12101278
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
1211-
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees
1212-
* shard, session and transaction compliance. In case an inaccurate count satisfies the applications needs use
1213-
* {@link #estimatedCount(String)} for empty queries instead.
1279+
* aggregation execution} which may have an impact on performance.
12141280
*
12151281
* @param query the {@link Query} class that specifies the criteria used to find documents. Must not be
12161282
* {@literal null}.

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

+92
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
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
import java.util.stream.Stream;
2728

@@ -185,6 +186,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
185186

186187
private SessionSynchronization sessionSynchronization = SessionSynchronization.ON_ACTUAL_TRANSACTION;
187188

189+
private CountExecution countExecution = this::doExactCount;
190+
188191
/**
189192
* Constructor used for a basic template configuration.
190193
*
@@ -338,6 +341,47 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) {
338341
this.entityCallbacks = entityCallbacks;
339342
}
340343

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

1016+
@Override
1017+
public long exactCount(Query query, @Nullable Class<?> entityClass, String collectionName) {
1018+
1019+
CountContext countContext = queryOperations.countQueryContext(query);
1020+
1021+
CountOptions options = countContext.getCountOptions(entityClass);
1022+
Document mappedQuery = countContext.getMappedQuery(entityClass, mappingContext::getPersistentEntity);
1023+
1024+
return doExactCount(collectionName, mappedQuery, options);
1025+
}
1026+
1027+
/*
1028+
* (non-Javadoc)
1029+
* @see org.springframework.data.mongodb.core.MongoOperations#count(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String)
1030+
*/
9721031
public long count(Query query, @Nullable Class<?> entityClass, String collectionName) {
9731032

9741033
Assert.notNull(query, "Query must not be null!");
@@ -990,10 +1049,33 @@ protected long doCount(String collectionName, Document filter, CountOptions opti
9901049
.debug(String.format("Executing count: %s in collection: %s", serializeToJsonSafely(filter), collectionName));
9911050
}
9921051

1052+
return countExecution.countDocuments(collectionName, filter, options);
1053+
}
1054+
1055+
protected long doExactCount(String collectionName, Document filter, CountOptions options) {
9931056
return execute(collectionName,
9941057
collection -> collection.countDocuments(CountQuery.of(filter).toQueryDocument(), options));
9951058
}
9961059

1060+
protected boolean countCanBeEstimated(Document filter, CountOptions options) {
1061+
1062+
return
1063+
// only empty filter for estimatedCount
1064+
filter.isEmpty() &&
1065+
// no skip, no limit,...
1066+
isEmptyOptions(options) &&
1067+
// transaction active?
1068+
!MongoDatabaseUtils.isTransactionActive(getMongoDatabaseFactory());
1069+
}
1070+
1071+
private boolean isEmptyOptions(CountOptions options) {
1072+
return options.getLimit() <= 0 && options.getSkip() <= 0;
1073+
}
1074+
1075+
/*
1076+
* (non-Javadoc)
1077+
* @see org.springframework.data.mongodb.core.MongoOperations#estimatedCount(java.lang.String)
1078+
*/
9971079
@Override
9981080
public long estimatedCount(String collectionName) {
9991081
return doEstimatedCount(collectionName, new EstimatedDocumentCountOptions());
@@ -3225,5 +3307,15 @@ public MongoDatabase getDb() {
32253307
// native MongoDB objects that offer methods with ClientSession must not be proxied.
32263308
return delegate.getDb();
32273309
}
3310+
3311+
@Override
3312+
protected boolean countCanBeEstimated(Document filter, CountOptions options) {
3313+
return false;
3314+
}
3315+
}
3316+
3317+
@FunctionalInterface
3318+
interface CountExecution {
3319+
long countDocuments(String collection, Document filter, CountOptions options);
32283320
}
32293321
}

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

+68-2
Original file line numberDiff line numberDiff line change
@@ -885,8 +885,11 @@ <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions
885885
* {@literal null}.
886886
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
887887
* @return the count of matching documents.
888+
* @since 3.4
888889
*/
889-
Mono<Long> count(Query query, Class<?> entityClass);
890+
default Mono<Long> exactCount(Query query, Class<?> entityClass) {
891+
return exactCount(query, entityClass, getCollectionName(entityClass));
892+
}
890893

891894
/**
892895
* Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query}
@@ -906,8 +909,11 @@ <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions
906909
* @param collectionName must not be {@literal null} or empty.
907910
* @return the count of matching documents.
908911
* @see #count(Query, Class, String)
912+
* @since 3.4
909913
*/
910-
Mono<Long> count(Query query, String collectionName);
914+
default Mono<Long> exactCount(Query query, String collectionName) {
915+
return exactCount(query, null, collectionName);
916+
}
911917

912918
/**
913919
* Returns the number of documents for the given {@link Query} by querying the given collection using the given entity
@@ -927,6 +933,66 @@ <S, T> Mono<T> findAndReplace(Query query, S replacement, FindAndReplaceOptions
927933
* @param entityClass the parametrized type. Can be {@literal null}.
928934
* @param collectionName must not be {@literal null} or empty.
929935
* @return the count of matching documents.
936+
* @since 3.4
937+
*/
938+
Mono<Long> exactCount(Query query, @Nullable Class<?> entityClass, String collectionName);
939+
940+
/**
941+
* Returns the number of documents for the given {@link Query} by querying the collection of the given entity class.
942+
* <br />
943+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
944+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
945+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
946+
* count all matches.
947+
* <br />
948+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
949+
* {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
950+
* aggregation execution} which may have an impact on performance.
951+
*
952+
* @param query the {@link Query} class that specifies the criteria used to find documents. Must not be
953+
* {@literal null}.
954+
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
955+
* @return the count of matching documents.
956+
*/
957+
Mono<Long> count(Query query, Class<?> entityClass);
958+
959+
/**
960+
* Returns the number of documents for the given {@link Query} querying the given collection. The given {@link Query}
961+
* must solely consist of document field references as we lack type information to map potential property references
962+
* onto document fields. Use {@link #count(Query, Class, String)} to get full type specific support. <br />
963+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
964+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
965+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
966+
* count all matches.
967+
* <br />
968+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
969+
* {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
970+
* aggregation execution} which may have an impact on performance.
971+
*
972+
* @param query the {@link Query} class that specifies the criteria used to find documents.
973+
* @param collectionName must not be {@literal null} or empty.
974+
* @return the count of matching documents.
975+
* @see #count(Query, Class, String)
976+
*/
977+
Mono<Long> count(Query query, String collectionName);
978+
979+
/**
980+
* Returns the number of documents for the given {@link Query} by querying the given collection using the given entity
981+
* class to map the given {@link Query}. <br />
982+
* <strong>NOTE:</strong> Query {@link Query#getSkip() offset} and {@link Query#getLimit() limit} can have direct
983+
* influence on the resulting number of documents found as those values are passed on to the server and potentially
984+
* limit the range and order within which the server performs the count operation. Use an {@literal unpaged} query to
985+
* count all matches.
986+
* <br />
987+
* This method may choose to use {@link #estimatedCount(Class)} for empty queries instead of running an
988+
* {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
989+
* aggregation execution} which may have an impact on performance.
990+
*
991+
* @param query the {@link Query} class that specifies the criteria used to find documents. Must not be
992+
* {@literal null}.
993+
* @param entityClass the parametrized type. Can be {@literal null}.
994+
* @param collectionName must not be {@literal null} or empty.
995+
* @return the count of matching documents.
930996
*/
931997
Mono<Long> count(Query query, @Nullable Class<?> entityClass, String collectionName);
932998

0 commit comments

Comments
 (0)