Skip to content

Commit c8e13ae

Browse files
committed
Refactor String-based repository aggretation methods into common utility callback.
Also, support aggregation result projections for reactive flows. See #4839 Original pull request: #4841
1 parent 6304af3 commit c8e13ae

File tree

8 files changed

+243
-170
lines changed

8 files changed

+243
-170
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java

+25-21
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import reactor.core.publisher.Flux;
1919
import reactor.core.publisher.Mono;
20+
import reactor.util.function.Tuple2;
2021

2122
import java.util.ArrayList;
2223
import java.util.List;
@@ -61,7 +62,6 @@
6162
import org.springframework.data.repository.query.ResultProcessor;
6263
import org.springframework.data.repository.query.ValueExpressionDelegate;
6364
import org.springframework.data.spel.ExpressionDependencies;
64-
import org.springframework.data.util.TypeInformation;
6565
import org.springframework.expression.ExpressionParser;
6666
import org.springframework.expression.spel.standard.SpelExpressionParser;
6767
import org.springframework.lang.Nullable;
@@ -70,7 +70,6 @@
7070
import org.springframework.util.StringUtils;
7171

7272
import com.mongodb.MongoClientSettings;
73-
import reactor.util.function.Tuple2;
7473

7574
/**
7675
* Base class for reactive {@link RepositoryQuery} implementations for MongoDB.
@@ -112,16 +111,20 @@ public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongo
112111
this.method = method;
113112
this.operations = operations;
114113
this.instantiators = new EntityInstantiators();
115-
this.valueExpressionDelegate = new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionParser.create(() -> expressionParser));
114+
this.valueExpressionDelegate = new ValueExpressionDelegate(
115+
new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(),
116+
evaluationContextProvider.getEvaluationContextProvider()),
117+
ValueExpressionParser.create(() -> expressionParser));
116118

117119
MongoEntityMetadata<?> metadata = method.getEntityInformation();
118120
Class<?> type = metadata.getCollectionEntity().getType();
119121

120122
this.findOperationWithProjection = operations.query(type);
121123
this.updateOps = operations.update(type);
122-
ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate.createValueContextProvider(
123-
method.getParameters());
124-
Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider, "ValueEvaluationContextProvider must be reactive");
124+
ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate
125+
.createValueContextProvider(method.getParameters());
126+
Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider,
127+
"ValueEvaluationContextProvider must be reactive");
125128
this.valueEvaluationContextProvider = (ReactiveValueEvaluationContextProvider) valueContextProvider;
126129
}
127130

@@ -151,9 +154,10 @@ public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongo
151154

152155
this.findOperationWithProjection = operations.query(type);
153156
this.updateOps = operations.update(type);
154-
ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate.createValueContextProvider(
155-
method.getParameters());
156-
Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider, "ValueEvaluationContextProvider must be reactive");
157+
ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate
158+
.createValueContextProvider(method.getParameters());
159+
Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider,
160+
"ValueEvaluationContextProvider must be reactive");
157161
this.valueEvaluationContextProvider = (ReactiveValueEvaluationContextProvider) valueContextProvider;
158162
}
159163

@@ -182,14 +186,9 @@ private Publisher<Object> execute(MongoParameterAccessor parameterAccessor) {
182186
ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(operations.getConverter(),
183187
parameterAccessor);
184188

185-
TypeInformation<?> returnType = method.getReturnType();
186189
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor);
187190
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
188191

189-
if (typeToRead == null && returnType.getComponentType() != null) {
190-
typeToRead = returnType.getComponentType().getType();
191-
}
192-
193192
return doExecute(method, processor, accessor, typeToRead);
194193
}
195194

@@ -221,11 +220,15 @@ protected Publisher<Object> doExecute(ReactiveMongoQueryMethod method, ResultPro
221220
String collection = method.getEntityInformation().getCollectionName();
222221

223222
ReactiveMongoQueryExecution execution = getExecution(accessor,
224-
new ResultProcessingConverter(processor, operations, instantiators), find);
223+
getResultProcessing(processor), find);
225224
return execution.execute(query, processor.getReturnedType().getDomainType(), collection);
226225
});
227226
}
228227

228+
ResultProcessingConverter getResultProcessing(ResultProcessor processor) {
229+
return new ResultProcessingConverter(processor, operations, instantiators);
230+
}
231+
229232
/**
230233
* Returns the execution instance to use.
231234
*
@@ -439,8 +442,8 @@ private Mono<Tuple2<ValueExpressionEvaluator, ParameterBindingDocumentCodec>> ex
439442
return getValueExpressionEvaluatorLater(dependencies, accessor).zipWith(Mono.just(codec));
440443
}
441444

442-
private Document decode(Tuple2<ValueExpressionEvaluator, ParameterBindingDocumentCodec> expressionEvaluator, String source, MongoParameterAccessor accessor,
443-
ParameterBindingDocumentCodec codec) {
445+
private Document decode(Tuple2<ValueExpressionEvaluator, ParameterBindingDocumentCodec> expressionEvaluator,
446+
String source, MongoParameterAccessor accessor, ParameterBindingDocumentCodec codec) {
444447

445448
ParameterBindingContext bindingContext = new ParameterBindingContext(accessor::getBindableValue,
446449
expressionEvaluator.getT1());
@@ -490,8 +493,8 @@ ValueExpressionEvaluator getValueExpressionEvaluator(MongoParameterAccessor acce
490493
@Override
491494
public <T> T evaluate(String expressionString) {
492495
ValueExpression expression = valueExpressionDelegate.parse(expressionString);
493-
ValueEvaluationContext evaluationContext = valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(),
494-
expression.getExpressionDependencies());
496+
ValueEvaluationContext evaluationContext = valueEvaluationContextProvider
497+
.getEvaluationContext(accessor.getValues(), expression.getExpressionDependencies());
495498
return (T) expression.evaluate(evaluationContext);
496499
}
497500
};
@@ -509,8 +512,9 @@ public <T> T evaluate(String expressionString) {
509512
protected Mono<ValueExpressionEvaluator> getValueExpressionEvaluatorLater(ExpressionDependencies dependencies,
510513
MongoParameterAccessor accessor) {
511514

512-
return valueEvaluationContextProvider.getEvaluationContextLater(accessor.getValues(), dependencies)
513-
.map(evaluationContext -> new ValueExpressionDelegateValueExpressionEvaluator(valueExpressionDelegate, valueExpression -> evaluationContext));
515+
return valueEvaluationContextProvider.getEvaluationContextLater(accessor.getValues(), dependencies)
516+
.map(evaluationContext -> new ValueExpressionDelegateValueExpressionEvaluator(valueExpressionDelegate,
517+
valueExpression -> evaluationContext));
514518
}
515519

516520
/**

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java

+122-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.time.Duration;
1919
import java.util.Map;
20+
import java.util.function.Function;
2021
import java.util.function.IntUnaryOperator;
2122
import java.util.function.LongUnaryOperator;
2223

@@ -28,11 +29,18 @@
2829
import org.springframework.data.mongodb.core.aggregation.Aggregation;
2930
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
3031
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
32+
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
33+
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
3134
import org.springframework.data.mongodb.core.convert.MongoConverter;
3235
import org.springframework.data.mongodb.core.mapping.FieldName;
36+
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
3337
import org.springframework.data.mongodb.core.query.Collation;
3438
import org.springframework.data.mongodb.core.query.Meta;
3539
import org.springframework.data.mongodb.core.query.Query;
40+
import org.springframework.data.repository.query.ResultProcessor;
41+
import org.springframework.data.repository.query.ReturnedType;
42+
import org.springframework.data.util.ReflectionUtils;
43+
import org.springframework.data.util.TypeInformation;
3644
import org.springframework.lang.Nullable;
3745
import org.springframework.util.ClassUtils;
3846
import org.springframework.util.ObjectUtils;
@@ -116,13 +124,15 @@ static AggregationOptions.Builder applyHint(AggregationOptions.Builder builder,
116124
}
117125

118126
/**
119-
* If present apply the preference from the {@link org.springframework.data.mongodb.repository.ReadPreference} annotation.
127+
* If present apply the preference from the {@link org.springframework.data.mongodb.repository.ReadPreference}
128+
* annotation.
120129
*
121130
* @param builder must not be {@literal null}.
122131
* @return never {@literal null}.
123132
* @since 4.2
124133
*/
125-
static AggregationOptions.Builder applyReadPreference(AggregationOptions.Builder builder, MongoQueryMethod queryMethod) {
134+
static AggregationOptions.Builder applyReadPreference(AggregationOptions.Builder builder,
135+
MongoQueryMethod queryMethod) {
126136

127137
if (!queryMethod.hasAnnotatedReadPreference()) {
128138
return builder;
@@ -131,6 +141,93 @@ static AggregationOptions.Builder applyReadPreference(AggregationOptions.Builder
131141
return builder.readPreference(ReadPreference.valueOf(queryMethod.getAnnotatedReadPreference()));
132142
}
133143

144+
static AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor,
145+
AggregationPipeline pipeline, ValueExpressionEvaluator evaluator) {
146+
147+
AggregationOptions.Builder builder = Aggregation.newAggregationOptions();
148+
149+
AggregationUtils.applyCollation(builder, method.getAnnotatedCollation(), accessor, evaluator);
150+
AggregationUtils.applyMeta(builder, method);
151+
AggregationUtils.applyHint(builder, method);
152+
AggregationUtils.applyReadPreference(builder, method);
153+
154+
TypeInformation<?> returnType = method.getReturnType();
155+
if (returnType.getComponentType() != null) {
156+
returnType = returnType.getRequiredComponentType();
157+
}
158+
if (ReflectionUtils.isVoid(returnType.getType()) && pipeline.isOutOrMerge()) {
159+
builder.skipOutput();
160+
}
161+
162+
return builder.build();
163+
}
164+
165+
/**
166+
* Prepares the AggregationPipeline including type discovery and calling {@link AggregationCallback} to run the
167+
* aggregation.
168+
*/
169+
@Nullable
170+
static <T> T doAggregate(AggregationPipeline pipeline, MongoQueryMethod method, ResultProcessor processor,
171+
ConvertingParameterAccessor accessor,
172+
Function<MongoParameterAccessor, ValueExpressionEvaluator> evaluatorFunction, AggregationCallback<T> callback) {
173+
174+
Class<?> sourceType = method.getDomainClass();
175+
ReturnedType returnedType = processor.getReturnedType();
176+
// 🙈Interface Projections do not happen on the Aggregation level but through our repository infrastructure.
177+
// Non-projections and raw results (AggregationResults<…>) are handled here. Interface projections read a Document
178+
// and DTO projections read the returned type.
179+
// We also support simple return types (String) that are read from a Document
180+
TypeInformation<?> returnType = method.getReturnType();
181+
Class<?> returnElementType = (returnType.getComponentType() != null ? returnType.getRequiredComponentType()
182+
: returnType).getType();
183+
Class<?> entityType;
184+
185+
boolean isRawAggregationResult = ClassUtils.isAssignable(AggregationResults.class, method.getReturnedObjectType());
186+
187+
if (returnElementType.equals(Document.class)) {
188+
entityType = sourceType;
189+
} else {
190+
entityType = returnElementType;
191+
}
192+
193+
AggregationUtils.appendSortIfPresent(pipeline, accessor, entityType);
194+
195+
if (method.isSliceQuery()) {
196+
AggregationUtils.appendLimitAndOffsetIfPresent(pipeline, accessor, LongUnaryOperator.identity(),
197+
limit -> limit + 1);
198+
} else {
199+
AggregationUtils.appendLimitAndOffsetIfPresent(pipeline, accessor);
200+
}
201+
202+
AggregationOptions options = AggregationUtils.computeOptions(method, accessor, pipeline,
203+
evaluatorFunction.apply(accessor));
204+
TypedAggregation<?> aggregation = new TypedAggregation<>(sourceType, pipeline.getOperations(), options);
205+
206+
boolean isSimpleReturnType = MongoSimpleTypes.HOLDER.isSimpleType(returnElementType);
207+
Class<?> typeToRead;
208+
209+
if (isSimpleReturnType) {
210+
typeToRead = Document.class;
211+
} else if (isRawAggregationResult) {
212+
typeToRead = returnElementType;
213+
} else {
214+
215+
if (returnedType.isProjecting()) {
216+
typeToRead = returnedType.getReturnedType().isInterface() ? Document.class : returnedType.getReturnedType();
217+
} else {
218+
typeToRead = entityType;
219+
}
220+
}
221+
222+
return callback.doAggregate(aggregation, sourceType, typeToRead, returnElementType, isSimpleReturnType,
223+
isRawAggregationResult);
224+
}
225+
226+
static AggregationPipeline computePipeline(AbstractMongoQuery mongoQuery, MongoQueryMethod method,
227+
ConvertingParameterAccessor accessor) {
228+
return new AggregationPipeline(mongoQuery.parseAggregationPipeline(method.getAnnotatedAggregation(), accessor));
229+
}
230+
134231
/**
135232
* Append {@code $sort} aggregation stage if {@link ConvertingParameterAccessor#getSort()} is present.
136233
*
@@ -139,7 +236,7 @@ static AggregationOptions.Builder applyReadPreference(AggregationOptions.Builder
139236
* @param targetType
140237
*/
141238
static void appendSortIfPresent(AggregationPipeline aggregationPipeline, ConvertingParameterAccessor accessor,
142-
Class<?> targetType) {
239+
@Nullable Class<?> targetType) {
143240

144241
if (accessor.getSort().isUnsorted()) {
145242
return;
@@ -254,4 +351,26 @@ private static <T> T getPotentiallyConvertedSimpleTypeValue(MongoConverter conve
254351

255352
return converter.getConversionService().convert(value, targetType);
256353
}
354+
355+
/**
356+
* Interface to invoke an aggregation along with source, intermediate, and target types.
357+
*
358+
* @param <T>
359+
*/
360+
interface AggregationCallback<T> {
361+
362+
/**
363+
* @param aggregation
364+
* @param domainType
365+
* @param typeToRead
366+
* @param elementType
367+
* @param simpleType whether the aggregation returns {@link Document} or a
368+
* {@link org.springframework.data.mapping.model.SimpleTypeHolder simple type}.
369+
* @param rawResult whether the aggregation returns {@link AggregationResults}.
370+
* @return
371+
*/
372+
@Nullable
373+
T doAggregate(TypedAggregation<?> aggregation, Class<?> domainType, Class<?> typeToRead, Class<?> elementType,
374+
boolean simpleType, boolean rawResult);
375+
}
257376
}

0 commit comments

Comments
 (0)