Skip to content

Commit 3b99fa0

Browse files
christophstroblmp911de
authored andcommitted
Skip output for void methods using declarative Aggregations having $out stage.
We now set the skipOutput flag if an annotated Aggregation defines an $out stage and when the method is declared to return no result (void / Mono<Void>, kotlin.Unit) Closes: #4088 Original pull request: #4341
1 parent 4b0c027 commit 3b99fa0

File tree

8 files changed

+101
-13
lines changed

8 files changed

+101
-13
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ public Object execute(Object[] parameters) {
127127
* @param accessor for providing invocation arguments. Never {@literal null}.
128128
* @param typeToRead the desired component target type. Can be {@literal null}.
129129
*/
130+
@Nullable
130131
protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor,
131132
@Nullable Class<?> typeToRead) {
132133

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.data.mongodb.core.aggregation.Aggregation;
2828
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
2929
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
30+
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
3031
import org.springframework.data.mongodb.core.convert.MongoConverter;
3132
import org.springframework.data.mongodb.core.query.Collation;
3233
import org.springframework.data.mongodb.core.query.Meta;
@@ -125,7 +126,7 @@ static AggregationOptions.Builder applyHint(AggregationOptions.Builder builder,
125126
* @param accessor
126127
* @param targetType
127128
*/
128-
static void appendSortIfPresent(List<AggregationOperation> aggregationPipeline, ConvertingParameterAccessor accessor,
129+
static void appendSortIfPresent(AggregationPipeline aggregationPipeline, ConvertingParameterAccessor accessor,
129130
Class<?> targetType) {
130131

131132
if (accessor.getSort().isUnsorted()) {
@@ -150,7 +151,7 @@ static void appendSortIfPresent(List<AggregationOperation> aggregationPipeline,
150151
* @param aggregationPipeline
151152
* @param accessor
152153
*/
153-
static void appendLimitAndOffsetIfPresent(List<AggregationOperation> aggregationPipeline,
154+
static void appendLimitAndOffsetIfPresent(AggregationPipeline aggregationPipeline,
154155
ConvertingParameterAccessor accessor) {
155156
appendLimitAndOffsetIfPresent(aggregationPipeline, accessor, LongUnaryOperator.identity(),
156157
IntUnaryOperator.identity());
@@ -166,7 +167,7 @@ static void appendLimitAndOffsetIfPresent(List<AggregationOperation> aggregation
166167
* @param limitOperator
167168
* @since 3.3
168169
*/
169-
static void appendLimitAndOffsetIfPresent(List<AggregationOperation> aggregationPipeline,
170+
static void appendLimitAndOffsetIfPresent(AggregationPipeline aggregationPipeline,
170171
ConvertingParameterAccessor accessor, LongUnaryOperator offsetOperator, IntUnaryOperator limitOperator) {
171172

172173
Pageable pageable = accessor.getPageable();

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

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.data.mongodb.core.query.UpdateDefinition;
3939
import org.springframework.data.support.PageableExecutionUtils;
4040
import org.springframework.data.util.TypeInformation;
41+
import org.springframework.lang.Nullable;
4142
import org.springframework.util.Assert;
4243
import org.springframework.util.ClassUtils;
4344

@@ -55,6 +56,7 @@
5556
@FunctionalInterface
5657
interface MongoQueryExecution {
5758

59+
@Nullable
5860
Object execute(Query query);
5961

6062
/**

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

+12-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.mongodb.repository.query;
1717

18+
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
19+
import org.springframework.data.util.ReflectionUtils;
1820
import reactor.core.publisher.Flux;
1921
import reactor.core.publisher.Mono;
2022

@@ -81,7 +83,7 @@ protected Publisher<Object> doExecute(ReactiveMongoQueryMethod method, ResultPro
8183
Class<?> sourceType = method.getDomainClass();
8284
Class<?> targetType = typeToRead;
8385

84-
List<AggregationOperation> pipeline = it;
86+
AggregationPipeline pipeline = new AggregationPipeline(it);
8587

8688
AggregationUtils.appendSortIfPresent(pipeline, accessor, typeToRead);
8789
AggregationUtils.appendLimitAndOffsetIfPresent(pipeline, accessor);
@@ -93,10 +95,13 @@ protected Publisher<Object> doExecute(ReactiveMongoQueryMethod method, ResultPro
9395
targetType = Document.class;
9496
}
9597

96-
AggregationOptions options = computeOptions(method, accessor);
97-
TypedAggregation<?> aggregation = new TypedAggregation<>(sourceType, pipeline, options);
98+
AggregationOptions options = computeOptions(method, accessor, pipeline);
99+
TypedAggregation<?> aggregation = new TypedAggregation<>(sourceType, pipeline.getOperations(), options);
98100

99101
Flux<?> flux = reactiveMongoOperations.aggregate(aggregation, targetType);
102+
if(ReflectionUtils.isVoid(typeToRead)) {
103+
return flux.then();
104+
}
100105

101106
if (isSimpleReturnType && !isRawReturnType) {
102107
flux = flux.handle((item, sink) -> {
@@ -121,14 +126,17 @@ private Mono<List<AggregationOperation>> computePipeline(ConvertingParameterAcce
121126
return parseAggregationPipeline(getQueryMethod().getAnnotatedAggregation(), accessor);
122127
}
123128

124-
private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor) {
129+
private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor, AggregationPipeline pipeline) {
125130

126131
AggregationOptions.Builder builder = Aggregation.newAggregationOptions();
127132

128133
AggregationUtils.applyCollation(builder, method.getAnnotatedCollation(), accessor, method.getParameters(),
129134
expressionParser, evaluationContextProvider);
130135
AggregationUtils.applyMeta(builder, method);
131136
AggregationUtils.applyHint(builder, method);
137+
if(ReflectionUtils.isVoid(method.getReturnType().getComponentType().getType()) && pipeline.isOutOrMerge()) {
138+
builder.skipOutput();
139+
}
132140

133141
return builder.build();
134142
}

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

+19-6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.data.mongodb.core.aggregation.Aggregation;
3030
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
3131
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
32+
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
3233
import org.springframework.data.mongodb.core.aggregation.AggregationOptions.Builder;
3334
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
3435
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
@@ -37,8 +38,12 @@
3738
import org.springframework.data.mongodb.core.query.Query;
3839
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
3940
import org.springframework.data.repository.query.ResultProcessor;
41+
import org.springframework.data.util.ReflectionUtils;
4042
import org.springframework.expression.ExpressionParser;
43+
import org.springframework.lang.Nullable;
4144
import org.springframework.util.ClassUtils;
45+
import org.springframework.util.CollectionUtils;
46+
import org.springframework.util.ObjectUtils;
4247

4348
/**
4449
* {@link AbstractMongoQuery} implementation to run string-based aggregations using
@@ -85,13 +90,14 @@ public StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOper
8590
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#doExecute(org.springframework.data.mongodb.repository.query.MongoQueryMethod, org.springframework.data.repository.query.ResultProcessor, org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor, java.lang.Class)
8691
*/
8792
@Override
93+
@Nullable
8894
protected Object doExecute(MongoQueryMethod method, ResultProcessor resultProcessor,
8995
ConvertingParameterAccessor accessor, Class<?> typeToRead) {
9096

9197
Class<?> sourceType = method.getDomainClass();
9298
Class<?> targetType = typeToRead;
9399

94-
List<AggregationOperation> pipeline = computePipeline(method, accessor);
100+
AggregationPipeline pipeline = computePipeline(method, accessor);
95101
AggregationUtils.appendSortIfPresent(pipeline, accessor, typeToRead);
96102

97103
if (method.isSliceQuery()) {
@@ -112,8 +118,8 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor resultProces
112118
targetType = method.getReturnType().getRequiredActualType().getRequiredComponentType().getType();
113119
}
114120

115-
AggregationOptions options = computeOptions(method, accessor);
116-
TypedAggregation<?> aggregation = new TypedAggregation<>(sourceType, pipeline, options);
121+
AggregationOptions options = computeOptions(method, accessor, pipeline);
122+
TypedAggregation<?> aggregation = new TypedAggregation<>(sourceType, pipeline.getOperations(), options);
117123

118124
if (method.isStreamQuery()) {
119125

@@ -127,6 +133,9 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor resultProces
127133
}
128134

129135
AggregationResults<Object> result = (AggregationResults<Object>) mongoOperations.aggregate(aggregation, targetType);
136+
if(ReflectionUtils.isVoid(typeToRead)) {
137+
return null;
138+
}
130139

131140
if (isRawAggregationResult) {
132141
return result;
@@ -168,11 +177,11 @@ private boolean isSimpleReturnType(Class<?> targetType) {
168177
return MongoSimpleTypes.HOLDER.isSimpleType(targetType);
169178
}
170179

171-
List<AggregationOperation> computePipeline(MongoQueryMethod method, ConvertingParameterAccessor accessor) {
172-
return parseAggregationPipeline(method.getAnnotatedAggregation(), accessor);
180+
AggregationPipeline computePipeline(MongoQueryMethod method, ConvertingParameterAccessor accessor) {
181+
return new AggregationPipeline(parseAggregationPipeline(method.getAnnotatedAggregation(), accessor));
173182
}
174183

175-
private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor) {
184+
private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor, AggregationPipeline pipeline) {
176185

177186
AggregationOptions.Builder builder = Aggregation.newAggregationOptions();
178187

@@ -181,6 +190,10 @@ private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingPar
181190
AggregationUtils.applyMeta(builder, method);
182191
AggregationUtils.applyHint(builder, method);
183192

193+
if(ReflectionUtils.isVoid(method.getReturnType().getType()) && pipeline.isOutOrMerge()) {
194+
builder.skipOutput();
195+
}
196+
184197
return builder.build();
185198
}
186199

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java

+28
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public class ReactiveStringBasedAggregationUnitTests {
7979

8080
private static final String RAW_SORT_STRING = "{ '$sort' : { 'lastname' : -1 } }";
8181
private static final String RAW_GROUP_BY_LASTNAME_STRING = "{ '$group': { '_id' : '$lastname', 'names' : { '$addToSet' : '$firstname' } } }";
82+
private static final String RAW_OUT = "{ '$out' : 'authors' }";
8283
private static final String GROUP_BY_LASTNAME_STRING_WITH_PARAMETER_PLACEHOLDER = "{ '$group': { '_id' : '$lastname', names : { '$addToSet' : '$?0' } } }";
8384
private static final String GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER = "{ '$group': { '_id' : '$lastname', 'names' : { '$addToSet' : '$?#{[0]}' } } }";
8485

@@ -196,6 +197,22 @@ private AggregationInvocation executeAggregation(String name, Object... args) {
196197
return new AggregationInvocation(aggregationCaptor.getValue(), targetTypeCaptor.getValue(), result);
197198
}
198199

200+
@Test // GH-4088
201+
void aggregateWithVoidReturnTypeSkipsResultOnOutStage() {
202+
203+
AggregationInvocation invocation = executeAggregation("outSkipResult");
204+
205+
assertThat(skipResultsOf(invocation)).isTrue();
206+
}
207+
208+
@Test // GH-4088
209+
void aggregateWithOutStageDoesNotSkipResults() {
210+
211+
AggregationInvocation invocation = executeAggregation("outDoNotSkipResult");
212+
213+
assertThat(skipResultsOf(invocation)).isFalse();
214+
}
215+
199216
private ReactiveStringBasedAggregation createAggregationForMethod(String name, Class<?>... parameters) {
200217

201218
Method method = ClassUtils.getMethod(SampleRepository.class, name, parameters);
@@ -230,6 +247,11 @@ private Object hintOf(AggregationInvocation invocation) {
230247
: null;
231248
}
232249

250+
private Boolean skipResultsOf(AggregationInvocation invocation) {
251+
return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults()
252+
: false;
253+
}
254+
233255
private Class<?> targetTypeOf(AggregationInvocation invocation) {
234256
return invocation.getTargetType();
235257
}
@@ -261,6 +283,12 @@ private interface SampleRepository extends ReactiveCrudRepository<Person, Long>
261283
@Hint("idx")
262284
@Aggregation(RAW_GROUP_BY_LASTNAME_STRING)
263285
String withHint();
286+
287+
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT })
288+
Flux<Person> outDoNotSkipResult();
289+
290+
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT })
291+
Mono<Void> outSkipResult();
264292
}
265293

266294
static class PersonAggregate {

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java

+28
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public class StringBasedAggregationUnitTests {
9292

9393
private static final String RAW_SORT_STRING = "{ '$sort' : { 'lastname' : -1 } }";
9494
private static final String RAW_GROUP_BY_LASTNAME_STRING = "{ '$group': { '_id' : '$lastname', 'names' : { '$addToSet' : '$firstname' } } }";
95+
private static final String RAW_OUT = "{ '$out' : 'authors' }";
9596
private static final String GROUP_BY_LASTNAME_STRING_WITH_PARAMETER_PLACEHOLDER = "{ '$group': { '_id' : '$lastname', names : { '$addToSet' : '$?0' } } }";
9697
private static final String GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER = "{ '$group': { '_id' : '$lastname', 'names' : { '$addToSet' : '$?#{[0]}' } } }";
9798

@@ -268,6 +269,22 @@ void aggregatePicksUpHintFromAnnotation() {
268269
assertThat(hintOf(invocation)).isEqualTo("idx");
269270
}
270271

272+
@Test // GH-4088
273+
void aggregateWithVoidReturnTypeSkipsResultOnOutStage() {
274+
275+
AggregationInvocation invocation = executeAggregation("outSkipResult");
276+
277+
assertThat(skipResultsOf(invocation)).isTrue();
278+
}
279+
280+
@Test // GH-4088
281+
void aggregateWithOutStageDoesNotSkipResults() {
282+
283+
AggregationInvocation invocation = executeAggregation("outDoNotSkipResult");
284+
285+
assertThat(skipResultsOf(invocation)).isFalse();
286+
}
287+
271288
private AggregationInvocation executeAggregation(String name, Object... args) {
272289

273290
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
@@ -316,6 +333,11 @@ private Object hintOf(AggregationInvocation invocation) {
316333
: null;
317334
}
318335

336+
private Boolean skipResultsOf(AggregationInvocation invocation) {
337+
return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults()
338+
: false;
339+
}
340+
319341
private Class<?> targetTypeOf(AggregationInvocation invocation) {
320342
return invocation.getTargetType();
321343
}
@@ -368,6 +390,12 @@ private interface SampleRepository extends Repository<Person, Long> {
368390
@Hint("idx")
369391
@Aggregation(RAW_GROUP_BY_LASTNAME_STRING)
370392
String withHint();
393+
394+
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT })
395+
List<Person> outDoNotSkipResult();
396+
397+
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT })
398+
void outSkipResult();
371399
}
372400

373401
private interface UnsupportedRepository extends Repository<Person, Long> {

src/main/asciidoc/reference/mongo-repositories-aggregation.adoc

+7
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ public interface PersonRepository extends CrudRepository<Person, String> {
3737
3838
@Aggregation("{ '$project': { '_id' : '$lastname' } }")
3939
List<String> findAllLastnames(); <9>
40+
41+
@Aggregation(pipeline = {
42+
"{ $group : { _id : '$author', books: { $push: '$title' } } }",
43+
"{ $out : 'authors' }"
44+
})
45+
void groupAndOutSkippingOutput(); <10>
4046
}
4147
----
4248
[source,java]
@@ -75,6 +81,7 @@ Therefore, the `Sort` properties are mapped against the methods return type `Per
7581
To gain more control, you might consider `AggregationResult` as method return type as shown in <7>.
7682
<8> Obtain the raw `AggregationResults` mapped to the generic target wrapper type `SumValue` or `org.bson.Document`.
7783
<9> Like in <6>, a single value can be directly obtained from multiple result ``Document``s.
84+
<10> Skips the output of the `$out` stage when return type is `void`.
7885
====
7986

8087
In some scenarios, aggregations might require additional options, such as a maximum run time, additional log comments, or the permission to temporarily write data to disk.

0 commit comments

Comments
 (0)