Skip to content

Commit 4812760

Browse files
committed
Polishing.
Refactor evaluation handling, align with JPA using MongoParameters and the originating Method. Add support for reactive GeoNear count. Hide Placeholder abstraction and create DocumentSerializer utility. See #5004 Original pull request: #5005
1 parent 13921be commit 4812760

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1185
-885
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,14 @@ interface TerminatingFindNear<T> {
194194
* @return never {@literal null}.
195195
*/
196196
Flux<GeoResult<T>> all();
197+
198+
/**
199+
* Count matching elements.
200+
*
201+
* @return number of elements matching the query.
202+
* @since 5.0
203+
*/
204+
Mono<Long> count();
197205
}
198206

199207
/**

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ public <R> TerminatingFindNear<R> map(QueryResultConverter<? super G, ? extends
239239
public Flux<GeoResult<G>> all() {
240240
return template.doGeoNear(nearQuery, domainType, getCollectionName(), returnType, resultConverter);
241241
}
242+
243+
@Override
244+
public Mono<Long> count() {
245+
return template.doGeoNearCount(nearQuery, domainType, getCollectionName());
246+
}
242247
}
243248

244249
/**

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

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.jspecify.annotations.Nullable;
4848
import org.reactivestreams.Publisher;
4949
import org.reactivestreams.Subscriber;
50+
5051
import org.springframework.beans.BeansException;
5152
import org.springframework.context.ApplicationContext;
5253
import org.springframework.context.ApplicationContextAware;
@@ -1026,30 +1027,38 @@ public <O> Flux<O> aggregate(Aggregation aggregation, String collectionName, Cla
10261027

10271028
protected <O> Flux<O> doAggregate(Aggregation aggregation, String collectionName, @Nullable Class<?> inputType,
10281029
Class<O> outputType) {
1029-
return doAggregate(aggregation, collectionName, inputType, outputType, QueryResultConverter.entity());
1030+
1031+
AggregationDefinition context = queryOperations.createAggregation(aggregation, inputType);
1032+
return doAggregate(aggregation, collectionName, outputType, QueryResultConverter.entity(), context);
10301033
}
10311034

10321035
<T, O> Flux<O> doAggregate(Aggregation aggregation, String collectionName, @Nullable Class<?> inputType,
10331036
Class<T> outputType, QueryResultConverter<? super T, ? extends O> resultConverter) {
10341037

1038+
AggregationDefinition context = queryOperations.createAggregation(aggregation, inputType);
1039+
return doAggregate(aggregation, collectionName, outputType, resultConverter, context);
1040+
}
1041+
1042+
<T, O> Flux<O> doAggregate(Aggregation aggregation, String collectionName, Class<T> outputType,
1043+
QueryResultConverter<? super T, ? extends O> resultConverter, AggregationDefinition definition) {
1044+
10351045
Assert.notNull(aggregation, "Aggregation pipeline must not be null");
10361046
Assert.hasText(collectionName, "Collection name must not be null or empty");
10371047
Assert.notNull(outputType, "Output type must not be null");
10381048

10391049
AggregationOptions options = aggregation.getOptions();
10401050
Assert.isTrue(!options.isExplain(), "Cannot use explain option with streaming");
10411051

1042-
AggregationDefinition ctx = queryOperations.createAggregation(aggregation, inputType);
10431052

10441053
if (LOGGER.isDebugEnabled()) {
10451054
LOGGER.debug(String.format("Streaming aggregation: %s in collection %s",
1046-
serializeToJsonSafely(ctx.getAggregationPipeline()), collectionName));
1055+
serializeToJsonSafely(definition.getAggregationPipeline()), collectionName));
10471056
}
10481057

10491058
DocumentCallback<O> readCallback = new QueryResultConverterCallback<>(resultConverter,
10501059
new ReadDocumentCallback<>(mongoConverter, outputType, collectionName));
1051-
return execute(collectionName, collection -> aggregateAndMap(collection, ctx.getAggregationPipeline(),
1052-
ctx.isOutOrMerge(), options, readCallback, ctx.getInputType()));
1060+
return execute(collectionName, collection -> aggregateAndMap(collection, definition.getAggregationPipeline(),
1061+
definition.isOutOrMerge(), options, readCallback, definition.getInputType()));
10531062
}
10541063

10551064
private <O> Flux<O> aggregateAndMap(MongoCollection<Document> collection, List<Document> pipeline,
@@ -1104,6 +1113,33 @@ protected <T> Flux<GeoResult<T>> geoNear(NearQuery near, Class<?> entityClass, S
11041113
return doGeoNear(near, entityClass, collectionName, returnType, QueryResultConverter.entity());
11051114
}
11061115

1116+
Mono<Long> doGeoNearCount(NearQuery near, Class<?> domainType, String collectionName) {
1117+
1118+
Builder optionsBuilder = AggregationOptions.builder().collation(near.getCollation());
1119+
1120+
if (near.hasReadPreference()) {
1121+
optionsBuilder.readPreference(near.getReadPreference());
1122+
}
1123+
1124+
if (near.hasReadConcern()) {
1125+
optionsBuilder.readConcern(near.getReadConcern());
1126+
}
1127+
1128+
String distanceField = operations.nearQueryDistanceFieldName(domainType);
1129+
Aggregation $geoNear = TypedAggregation.newAggregation(domainType,
1130+
Aggregation.geoNear(near, distanceField).skip(-1).limit(-1), Aggregation.count().as("_totalCount"))
1131+
.withOptions(optionsBuilder.build());
1132+
1133+
AggregationDefinition definition = queryOperations.createAggregation($geoNear, (AggregationOperationContext) null);
1134+
1135+
Flux<Document> results = doAggregate($geoNear, collectionName, Document.class, QueryResultConverter.entity(),
1136+
definition);
1137+
1138+
return results.last()
1139+
.map(doc -> NumberUtils.convertNumberToTargetClass(doc.get("_totalCount", Integer.class), Long.class))
1140+
.defaultIfEmpty(0L);
1141+
}
1142+
11071143
@SuppressWarnings("unchecked")
11081144
<T, R> Flux<GeoResult<R>> doGeoNear(NearQuery near, Class<?> entityClass, String collectionName, Class<T> returnType,
11091145
QueryResultConverter<? super T, ? extends R> resultConverter) {

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

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -343,13 +343,7 @@ public Criteria in(@Nullable Object ... values) {
343343
*/
344344
@Contract("_ -> this")
345345
public Criteria in(Collection<?> values) {
346-
347-
ArrayList<?> objects = new ArrayList<>(values);
348-
if (objects.size() == 1 && CollectionUtils.firstElement(objects) instanceof Placeholder placeholder) {
349-
criteria.put("$in", placeholder);
350-
} else {
351-
criteria.put("$in", objects);
352-
}
346+
criteria.put("$in", values);
353347
return this;
354348
}
355349

@@ -374,13 +368,7 @@ public Criteria nin(Object... values) {
374368
*/
375369
@Contract("_ -> this")
376370
public Criteria nin(Collection<?> values) {
377-
378-
ArrayList<?> objects = new ArrayList<>(values);
379-
if (objects.size() == 1 && CollectionUtils.firstElement(objects) instanceof Placeholder placeholder) {
380-
criteria.put("$nin", placeholder);
381-
} else {
382-
criteria.put("$nin", objects);
383-
}
371+
criteria.put("$nin", values);
384372
return this;
385373
}
386374

@@ -931,7 +919,7 @@ public Criteria andOperator(Collection<Criteria> criteria) {
931919
*
932920
* @param operator the native MongoDB operator.
933921
* @param value the operator value
934-
* @return this
922+
* @return this.
935923
* @since 5.0
936924
*/
937925
@Contract("_, _ -> this")

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

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -40,45 +40,4 @@ public interface CriteriaDefinition {
4040
@Nullable
4141
String getKey();
4242

43-
/**
44-
* A placeholder expression used when rending queries to JSON.
45-
*
46-
* @since 5.0
47-
* @author Christoph Strobl
48-
*/
49-
interface Placeholder {
50-
51-
/**
52-
* Create a new placeholder for index bindable parameter.
53-
*
54-
* @param position the index of the parameter to bind.
55-
* @return new instance of {@link Placeholder}.
56-
*/
57-
static Placeholder indexed(int position) {
58-
return new PlaceholderImpl("?%s".formatted(position));
59-
}
60-
61-
static Placeholder placeholder(String expression) {
62-
return new PlaceholderImpl(expression);
63-
}
64-
65-
Object getValue();
66-
}
67-
68-
static class PlaceholderImpl implements Placeholder {
69-
private final Object expression;
70-
71-
public PlaceholderImpl(Object expression) {
72-
this.expression = expression;
73-
}
74-
75-
@Override
76-
public Object getValue() {
77-
return expression;
78-
}
79-
80-
public String toString() {
81-
return getValue().toString();
82-
}
83-
}
8443
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,9 @@ private String getCommand(Shape shape) {
7676

7777
Assert.notNull(shape, "Shape must not be null");
7878

79-
if(shape instanceof GeoJson<?>) {
79+
if (shape instanceof GeoJson<?>) {
8080
return "$geometry";
81-
}
82-
if (shape instanceof Box) {
81+
} else if (shape instanceof Box) {
8382
return "$box";
8483
} else if (shape instanceof Circle) {
8584
return "$center";

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import org.bson.BsonRegularExpression;
2121
import org.jspecify.annotations.Nullable;
2222

23+
import org.springframework.lang.Contract;
24+
2325
/**
2426
* @author Christoph Strobl
2527
* @author Mark Paluch
@@ -80,6 +82,7 @@ public enum MatchMode {
8082
* @param matcherType the type of matching to perform
8183
* @return {@literal source} when {@literal source} or {@literal matcherType} is {@literal null}.
8284
*/
85+
@Contract("null, null -> null;null, _ -> null;_, null -> null;_ , _ -> !null")
8386
public @Nullable String toRegularExpression(@Nullable String source, @Nullable MatchMode matcherType) {
8487

8588
if (matcherType == null || source == null) {

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
package org.springframework.data.mongodb.repository.aot;
1717

1818
import java.util.ArrayList;
19-
import java.util.LinkedHashMap;
19+
import java.util.Collection;
2020
import java.util.List;
21-
import java.util.Map;
2221
import java.util.stream.Stream;
2322

2423
import org.bson.Document;
2524
import org.jspecify.annotations.NullUnmarked;
25+
2626
import org.springframework.core.ResolvableType;
2727
import org.springframework.core.annotation.MergedAnnotation;
2828
import org.springframework.data.domain.SliceImpl;
@@ -47,6 +47,8 @@
4747
import org.springframework.util.StringUtils;
4848

4949
/**
50+
* Code blocks for building aggregation pipelines and execution statements for MongoDB repositories.
51+
*
5052
* @author Christoph Strobl
5153
* @since 5.0
5254
*/
@@ -160,7 +162,7 @@ static class AggregationCodeBlockBuilder {
160162

161163
private final AotQueryMethodGenerationContext context;
162164
private final MongoQueryMethod queryMethod;
163-
private final Map<String, CodeBlock> arguments;
165+
private final String parameterNames;
164166

165167
private AggregationInteraction source;
166168

@@ -170,9 +172,8 @@ static class AggregationCodeBlockBuilder {
170172
AggregationCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
171173

172174
this.context = context;
173-
this.arguments = new LinkedHashMap<>();
174-
context.getBindableParameterNames().forEach(it -> arguments.put(it, CodeBlock.of(it)));
175175
this.queryMethod = queryMethod;
176+
this.parameterNames = StringUtils.collectionToDelimitedString(context.getAllParameterNames(), ", ");
176177
}
177178

178179
AggregationCodeBlockBuilder stages(AggregationInteraction aggregation) {
@@ -220,33 +221,18 @@ private CodeBlock pipeline(String pipelineVariableName) {
220221
String limitParameter = context.getLimitParameterName();
221222
String pageableParameter = context.getPageableParameterName();
222223

223-
boolean mightBeSorted = StringUtils.hasText(sortParameter);
224-
boolean mightBeLimited = StringUtils.hasText(limitParameter);
225-
boolean mightBePaged = StringUtils.hasText(pageableParameter);
226-
227-
int stageCount = source.stages().size();
228-
if (mightBeSorted) {
229-
stageCount++;
230-
}
231-
if (mightBeLimited) {
232-
stageCount++;
233-
}
234-
if (mightBePaged) {
235-
stageCount += 3;
236-
}
237-
238224
Builder builder = CodeBlock.builder();
239-
builder.add(aggregationStages(context.localVariable("stages"), source.stages(), stageCount, arguments));
225+
builder.add(aggregationStages(context.localVariable("stages"), source.stages()));
240226

241-
if (mightBeSorted) {
227+
if (StringUtils.hasText(sortParameter)) {
242228
builder.add(sortingStage(sortParameter));
243229
}
244230

245-
if (mightBeLimited) {
231+
if (StringUtils.hasText(limitParameter)) {
246232
builder.add(limitingStage(limitParameter));
247233
}
248234

249-
if (mightBePaged) {
235+
if (StringUtils.hasText(pageableParameter)) {
250236
builder.add(pagingStage(pageableParameter, queryMethod.isSliceQuery()));
251237
}
252238

@@ -259,6 +245,7 @@ private CodeBlock aggregationOptions(String aggregationVariableName) {
259245

260246
Builder builder = CodeBlock.builder();
261247
List<CodeBlock> options = new ArrayList<>(5);
248+
262249
if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) {
263250
options.add(CodeBlock.of(".skipOutput()"));
264251
}
@@ -299,20 +286,21 @@ private CodeBlock aggregationOptions(String aggregationVariableName) {
299286
return builder.build();
300287
}
301288

302-
private CodeBlock aggregationStages(String stageListVariableName, Iterable<String> stages, int stageCount,
303-
Map<String, CodeBlock> arguments) {
289+
private CodeBlock aggregationStages(String stageListVariableName, Collection<String> stages) {
304290

305291
Builder builder = CodeBlock.builder();
306292
builder.addStatement("$T<$T> $L = new $T($L)", List.class, Object.class, stageListVariableName, ArrayList.class,
307-
stageCount);
293+
stages.size());
308294
int stageCounter = 0;
309295

310296
for (String stage : stages) {
311297

312298
VariableSnippet stageSnippet = Snippet.declare(builder)
313-
.variable(Document.class, context.localVariable("stage_%s".formatted(stageCounter++)))
314-
.of(MongoCodeBlocks.asDocument(stage, arguments));
299+
.variable(Document.class, context.localVariable("stage_%s".formatted(stageCounter)))
300+
.of(MongoCodeBlocks.asDocument(stage, parameterNames));
315301
builder.addStatement("$L.add($L)", stageListVariableName, stageSnippet.getVariableName());
302+
303+
stageCounter++;
316304
}
317305

318306
return builder.build();

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@
2323

2424
/**
2525
* An {@link MongoInteraction aggregation interaction}.
26-
*
26+
*
2727
* @author Christoph Strobl
2828
* @since 5.0
2929
*/
3030
class AggregationInteraction extends MongoInteraction implements QueryMetadata {
3131

32-
private final StringAggregation aggregation;
32+
private final AotStringAggregation aggregation;
3333

3434
AggregationInteraction(String[] raw) {
35-
this.aggregation = new StringAggregation(raw);
35+
this.aggregation = new AotStringAggregation(raw);
3636
}
3737

3838
List<String> stages() {
@@ -46,7 +46,6 @@ InteractionType getExecutionType() {
4646

4747
@Override
4848
public Map<String, Object> serialize() {
49-
5049
return Map.of(pipelineSerializationKey(), stages());
5150
}
5251

0 commit comments

Comments
 (0)