Skip to content

Commit 133d159

Browse files
DATAMONGO-2153 - Annotated aggregation support.
The repository layer offers means interact with the aggregation framework via annotated repository finder methods. Similar to the JSON based queries a pipeline can be defined via the Aggregation annotation. The definition may contain simple placeholders like `?0` as well as SpEL expression markers `?#{ ... }`. public interface PersonRepository extends CrudReppsitory<Person, String> { @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $?0 } } }") List<PersonAggregate> groupByLastnameAnd(String property); @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }") List<PersonAggregate> groupByLastnameAndFirstnames(Sort sort); @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $?0 } } }") List<PersonAggregate> groupByLastnameAnd(String property, Pageable page); @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }") SumValue sumAgeUsingValueWrapper(); @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }") Long sumAge(); @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }") AggregationResults<SumValue> sumAgeRaw(); @Aggregation("{ '$project': { '_id' : '$lastname' } }") List<String> findAllLastnames(); } public interface ReactivePersonRepository extends ReactiveCrudReppsitory<Person, String> { @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $?0 } } }") Flux<PersonAggregate> groupByLastnameAnd(String property); @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }") Mono<Long> sumAge(); @Aggregation("{ '$project': { '_id' : '$lastname' } }") Flux<String> findAllLastnames(); }
1 parent 8e24236 commit 133d159

30 files changed

+1891
-106
lines changed

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import org.bson.Document;
1919
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
20+
import org.springframework.lang.Nullable;
2021

2122
/**
2223
* The context for an {@link AggregationOperation}.
@@ -33,7 +34,20 @@ public interface AggregationOperationContext {
3334
* @param document will never be {@literal null}.
3435
* @return must not be {@literal null}.
3536
*/
36-
Document getMappedObject(Document document);
37+
default Document getMappedObject(Document document) {
38+
return getMappedObject(document, null);
39+
}
40+
41+
/**
42+
* Returns the mapped {@link Document}, potentially converting the source considering mapping metadata for the given
43+
* type.
44+
*
45+
* @param document will never be {@literal null}.
46+
* @param type can be {@literal null}.
47+
* @return must not be {@literal null}.
48+
* @since 2.2
49+
*/
50+
Document getMappedObject(Document document, @Nullable Class<?> type);
3751

3852
/**
3953
* Returns a {@link FieldReference} for the given field or {@literal null} if the context does not expose the given

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
2525
import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField;
2626
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
27+
import org.springframework.lang.Nullable;
2728

2829
/**
2930
* Rendering support for {@link AggregationOperation} into a {@link List} of {@link org.bson.Document}.
@@ -75,15 +76,16 @@ static List<Document> toDocument(List<AggregationOperation> operations, Aggregat
7576
* Simple {@link AggregationOperationContext} that just returns {@link FieldReference}s as is.
7677
*
7778
* @author Oliver Gierke
79+
* @author Christoph Strobl
7880
*/
7981
private static class NoOpAggregationOperationContext implements AggregationOperationContext {
8082

8183
/*
8284
* (non-Javadoc)
83-
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(org.bson.Document)
85+
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(org.bson.Document, java.lang.Class)
8486
*/
8587
@Override
86-
public Document getMappedObject(Document document) {
88+
public Document getMappedObject(Document document, @Nullable Class<?> type) {
8789
return document;
8890
}
8991

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ public ExposedFieldsAggregationOperationContext(ExposedFields exposedFields,
5656

5757
/*
5858
* (non-Javadoc)
59-
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(org.bson.Document)
59+
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(org.bson.Document, java.lang.Class)
6060
*/
6161
@Override
62-
public Document getMappedObject(Document document) {
63-
return rootContext.getMappedObject(document);
62+
public Document getMappedObject(Document document, @Nullable Class<?> type) {
63+
return rootContext.getMappedObject(document, type);
6464
}
6565

6666
/*

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ public NestedDelegatingExpressionAggregationOperationContext(AggregationOperatio
4545

4646
/*
4747
* (non-Javadoc)
48-
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(org.bson.Document)
48+
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(org.bson.Document, java.lang.Class)
4949
*/
5050
@Override
51-
public Document getMappedObject(Document document) {
52-
return delegate.getMappedObject(document);
51+
public Document getMappedObject(Document document, Class<?> type) {
52+
return delegate.getMappedObject(document, type);
5353
}
5454

5555
/*

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import org.bson.Document;
2727
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
28+
import org.springframework.lang.Nullable;
2829

2930
/**
3031
* {@link AggregationOperationContext} implementation prefixing non-command keys on root level with the given prefix.
@@ -56,11 +57,11 @@ public PrefixingDelegatingAggregationOperationContext(AggregationOperationContex
5657

5758
/*
5859
* (non-Javadoc)
59-
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(org.bson.Document)
60+
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(org.bson.Document, java.lang.Class)
6061
*/
6162
@Override
62-
public Document getMappedObject(Document document) {
63-
return doPrefix(delegate.getMappedObject(document));
63+
public Document getMappedObject(Document document, @Nullable Class<?> type) {
64+
return doPrefix(delegate.getMappedObject(document, type));
6465
}
6566

6667
/*

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.data.mongodb.core.convert.QueryMapper;
2828
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
2929
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
30+
import org.springframework.lang.Nullable;
3031
import org.springframework.util.Assert;
3132

3233
/**
@@ -70,7 +71,16 @@ public TypeBasedAggregationOperationContext(Class<?> type,
7071
*/
7172
@Override
7273
public Document getMappedObject(Document document) {
73-
return mapper.getMappedObject(document, mappingContext.getPersistentEntity(type));
74+
return getMappedObject(document, type);
75+
}
76+
77+
/*
78+
* (non-Javadoc)
79+
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(org.bson.Document, java.lang.Class)
80+
*/
81+
@Override
82+
public Document getMappedObject(Document document, @Nullable Class<?> type) {
83+
return mapper.getMappedObject(document, type != null ? mappingContext.getPersistentEntity(type) : null);
7484
}
7585

7686
/*
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.repository;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
import org.springframework.core.annotation.AliasFor;
25+
import org.springframework.data.annotation.QueryAnnotation;
26+
27+
/**
28+
* The {@link Aggregation} annotation can be used to decorate a {@link org.springframework.data.repository.Repository}
29+
* query method so that it runs the {@link Aggregation#pipeline()} on invocation. <br />
30+
* The pipeline stages are mapped against the {@link org.springframework.data.repository.Repository} domain type to
31+
* consider {@link org.springframework.data.mongodb.core.mapping.Field field} mappings and may contain simple
32+
* placeholders {@code ?0} as well as {@link org.springframework.expression.spel.standard.SpelExpression
33+
* SpelExpressions}. <br />
34+
* Query method {@link org.springframework.data.domain.Sort} and {@link org.springframework.data.domain.Pageable}
35+
* arguments are applied at the end of the pipeline or can be defined manually as part of it.
36+
*
37+
* @author Christoph Strobl
38+
* @since 2.2
39+
*/
40+
@Retention(RetentionPolicy.RUNTIME)
41+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
42+
@Documented
43+
@QueryAnnotation
44+
public @interface Aggregation {
45+
46+
/**
47+
* Alias for {@link #pipeline()}. Defines the aggregation pipeline to apply.
48+
*
49+
* @return an empty array by default.
50+
* @see #pipeline()
51+
*/
52+
@AliasFor("pipeline")
53+
String[] value() default {};
54+
55+
/**
56+
* Defines the aggregation pipeline to apply.
57+
*
58+
* <pre class="code">
59+
*
60+
* // aggregation resulting in collection with single value
61+
* &#64;Aggregation("{ '$project': { '_id' : '$lastname' } }")
62+
* List<String> findAllLastnames();
63+
*
64+
* // aggregation with parameter replacement
65+
* &#64;Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }")
66+
* List<PersonAggregate> groupByLastnameAnd(String property);
67+
*
68+
* // aggregation with sort in pipeline
69+
* &#64;Aggregation(pipeline = {"{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }", "{ '$sort' : { 'lastname' : -1 } }"})
70+
* List<PersonAggregate> groupByLastnameAnd(String property);
71+
*
72+
* // Sort parameter is used for sorting results
73+
* &#64;Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }")
74+
* List<PersonAggregate> groupByLastnameAnd(String property, Sort sort);
75+
*
76+
* // Pageable parameter used for sort, skip and limit
77+
* &#64;Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }")
78+
* List<PersonAggregate> groupByLastnameAnd(String property, Pageable page);
79+
*
80+
* // Single value result aggregation.
81+
* &#64;Aggregation("{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }")
82+
* Long sumAge();
83+
*
84+
* // Single value wrapped in container object
85+
* &#64;Aggregation("{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } })
86+
* SumAge sumAgeAndReturnAggregationResultWrapperWithConcreteType();
87+
*
88+
* // Raw aggregation result
89+
* &#64;Aggregation("{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } })
90+
* AggregationResults&lt;org.bson.Document>&gt; sumAgeAndReturnAggregationResultWrapper();
91+
* </pre>
92+
*
93+
* @return an empty array by default.
94+
*/
95+
@AliasFor("value")
96+
String[] pipeline() default {};
97+
98+
/**
99+
* Defines the collation to apply when executing the aggregation.
100+
*
101+
* <pre class="code">
102+
* // Fixed value
103+
* &#64;Aggregation(pipeline = "...", collation = "en_US")
104+
* List<Entry> findAllByFixedCollation();
105+
*
106+
* // Fixed value as Document
107+
* &#64;Aggregation(pipeline = "...", collation = "{ 'locale' : 'en_US' }")
108+
* List<Entry> findAllByFixedJsonCollation();
109+
*
110+
* // Dynamic value as String
111+
* &#64;Aggregation(pipeline = "...", collation = "?0")
112+
* List<Entry> findAllByDynamicCollation(String collation);
113+
*
114+
* // Dynamic value as Document
115+
* &#64;Aggregation(pipeline = "...", collation = "{ 'locale' : ?0 }")
116+
* List<Entry> findAllByDynamicJsonCollation(String collation);
117+
*
118+
* // SpEL expression
119+
* &#64;Aggregation(pipeline = "...", collation = "?#{[0]}")
120+
* List<Entry> findAllByDynamicSpElCollation(String collation);
121+
* </pre>
122+
*
123+
* @return an empty {@link String} by default.
124+
* @since 2.2
125+
*/
126+
String collation() default "";
127+
}

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package org.springframework.data.mongodb.repository.query;
1717

1818
import org.bson.Document;
19-
2019
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
2120
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
2221
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
@@ -32,6 +31,7 @@
3231
import org.springframework.data.repository.query.RepositoryQuery;
3332
import org.springframework.data.repository.query.ResultProcessor;
3433
import org.springframework.expression.spel.standard.SpelExpressionParser;
34+
import org.springframework.lang.Nullable;
3535
import org.springframework.util.Assert;
3636

3737
/**
@@ -94,22 +94,36 @@ public Object execute(Object[] parameters) {
9494

9595
ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(operations.getConverter(),
9696
new MongoParametersParameterAccessor(method, parameters));
97+
98+
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor);
99+
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
100+
101+
return processor.processResult(doExecute(method, processor, accessor, typeToRead));
102+
}
103+
104+
/**
105+
* Execute the {@link RepositoryQuery} of the given method with the parameters provided by the
106+
* {@link ConvertingParameterAccessor accessor}
107+
*
108+
* @param method the {@link MongoQueryMethod} invoked. Never {@literal null}.
109+
* @param processor {@link ResultProcessor} for post procession. Never {@literal null}.
110+
* @param accessor for providing invocation arguments. Never {@literal null}.
111+
* @param typeToRead the desired component target type. Can be {@literal null}.
112+
*/
113+
protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor,
114+
@Nullable Class<?> typeToRead) {
115+
97116
Query query = createQuery(accessor);
98117

99118
applyQueryMetaAttributesWhenPresent(query);
100119
query = applyAnnotatedDefaultSortIfPresent(query);
101120
query = applyAnnotatedCollationIfPresent(query, accessor);
102121

103-
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor);
104-
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
105-
106122
FindWithQuery<?> find = typeToRead == null //
107123
? executableFind //
108124
: executableFind.as(typeToRead);
109125

110-
MongoQueryExecution execution = getExecution(accessor, find);
111-
112-
return processor.processResult(execution.execute(query));
126+
return getExecution(accessor, find).execute(query);
113127
}
114128

115129
private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery<?> operation) {

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
import org.bson.Document;
2222
import org.reactivestreams.Publisher;
23-
2423
import org.springframework.core.convert.converter.Converter;
2524
import org.springframework.data.convert.EntityInstantiators;
2625
import org.springframework.data.mongodb.core.MongoOperations;
@@ -38,6 +37,7 @@
3837
import org.springframework.data.repository.query.RepositoryQuery;
3938
import org.springframework.data.repository.query.ResultProcessor;
4039
import org.springframework.expression.spel.standard.SpelExpressionParser;
40+
import org.springframework.lang.Nullable;
4141
import org.springframework.util.Assert;
4242

4343
/**
@@ -119,24 +119,39 @@ private Object execute(MongoParameterAccessor parameterAccessor) {
119119

120120
ConvertingParameterAccessor convertingParamterAccessor = new ConvertingParameterAccessor(operations.getConverter(),
121121
parameterAccessor);
122-
Query query = createQuery(convertingParamterAccessor);
123-
124-
applyQueryMetaAttributesWhenPresent(query);
125-
query = applyAnnotatedDefaultSortIfPresent(query);
126-
query = applyAnnotatedCollationIfPresent(query, convertingParamterAccessor);
127122

128123
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(convertingParamterAccessor);
129124
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
130125

126+
return doExecute(method, processor, convertingParamterAccessor, typeToRead);
127+
}
128+
129+
/**
130+
* Execute the {@link RepositoryQuery} of the given method with the parameters provided by the
131+
* {@link ConvertingParameterAccessor accessor}
132+
*
133+
* @param method the {@link ReactiveMongoQueryMethod} invoked. Never {@literal null}.
134+
* @param processor {@link ResultProcessor} for post procession. Never {@literal null}.
135+
* @param accessor for providing invocation arguments. Never {@literal null}.
136+
* @param typeToRead the desired component target type. Can be {@literal null}.
137+
*/
138+
protected Object doExecute(ReactiveMongoQueryMethod method, ResultProcessor processor,
139+
ConvertingParameterAccessor accessor, @Nullable Class<?> typeToRead) {
140+
141+
Query query = createQuery(accessor);
142+
143+
applyQueryMetaAttributesWhenPresent(query);
144+
query = applyAnnotatedDefaultSortIfPresent(query);
145+
query = applyAnnotatedCollationIfPresent(query, accessor);
146+
131147
FindWithQuery<?> find = typeToRead == null //
132148
? findOperationWithProjection //
133149
: findOperationWithProjection.as(typeToRead);
134150

135151
String collection = method.getEntityInformation().getCollectionName();
136152

137-
ReactiveMongoQueryExecution execution = getExecution(convertingParamterAccessor,
153+
ReactiveMongoQueryExecution execution = getExecution(accessor,
138154
new ResultProcessingConverter(processor, operations, instantiators), find);
139-
140155
return execution.execute(query, processor.getReturnedType().getDomainType(), collection);
141156
}
142157

0 commit comments

Comments
 (0)