DATAMONGO-2153 - Polishing.

Use MongoQueryMethod.getDomainClass() instead of getRepositoryDomainType(). Simplify annotation presence indicator methods hasAnnotatedSort() and hasAnnotatedCollation(). Refactor getAnnotatedAggregation() to non-nullable method throwing IllegalStateException to be consistent with other getXxx() methods.

Simplify aggregation execution and consider collection/single element declaration for reactive execution.

Tweak docs.

Original pull request: #743.
This commit is contained in:
Mark Paluch
2019-05-16 16:21:00 +02:00
parent 221ffb1947
commit f456851791
13 changed files with 148 additions and 132 deletions

View File

@@ -25,12 +25,13 @@ import org.springframework.core.annotation.AliasFor;
import org.springframework.data.annotation.QueryAnnotation;
/**
* The {@link Aggregation} annotation can be used to decorate a {@link org.springframework.data.repository.Repository}
* query method so that it runs the {@link Aggregation#pipeline()} on invocation. <br />
* The pipeline stages are mapped against the {@link org.springframework.data.repository.Repository} domain type to
* consider {@link org.springframework.data.mongodb.core.mapping.Field field} mappings and may contain simple
* placeholders {@code ?0} as well as {@link org.springframework.expression.spel.standard.SpelExpression
* SpelExpressions}. <br />
* The {@link Aggregation} annotation can be used to annotate a {@link org.springframework.data.repository.Repository}
* query method so that it runs the {@link Aggregation#pipeline()} on invocation.
* <p />
* Pipeline stages are mapped against the {@link org.springframework.data.repository.Repository} domain type to consider
* {@link org.springframework.data.mongodb.core.mapping.Field field} mappings and may contain simple placeholders
* {@code ?0} as well as {@link org.springframework.expression.spel.standard.SpelExpression SpelExpressions}.
* <p />
* Query method {@link org.springframework.data.domain.Sort} and {@link org.springframework.data.domain.Pageable}
* arguments are applied at the end of the pipeline or can be defined manually as part of it.
*
@@ -121,7 +122,6 @@ public @interface Aggregation {
* </pre>
*
* @return an empty {@link String} by default.
* @since 2.2
*/
String collation() default "";
}

View File

@@ -62,7 +62,6 @@ class AggregationUtils {
* @see AggregationOptions#getCollation()
* @see CollationUtils#computeCollation(String, ConvertingParameterAccessor, MongoParameters, SpelExpressionParser,
* QueryMethodEvaluationContextProvider)
* @since 2.2
*/
static AggregationOptions.Builder applyCollation(AggregationOptions.Builder builder,
@Nullable String collationExpression, ConvertingParameterAccessor accessor, MongoParameters parameters,
@@ -79,7 +78,7 @@ class AggregationUtils {
* {@link ParameterBindingDocumentCodec} to obtain the MongoDB native {@link Document} representation returned by
* {@link AggregationOperation#toDocument(AggregationOperationContext)} that is mapped against the domain type
* properties.
*
*
* @param method
* @param accessor
* @param expressionParser
@@ -94,7 +93,7 @@ class AggregationUtils {
List<AggregationOperation> target = new ArrayList<>(method.getAnnotatedAggregation().length);
for (String source : method.getAnnotatedAggregation()) {
target.add(ctx -> ctx.getMappedObject(CODEC.decode(source, bindingContext), method.getRepositoryDomainType()));
target.add(ctx -> ctx.getMappedObject(CODEC.decode(source, bindingContext), method.getDomainClass()));
}
return target;
}
@@ -174,16 +173,16 @@ class AggregationUtils {
return getPotentiallyConvertedSimpleTypeValue(converter, source.values().iterator().next(), targetType);
}
Document tmp = new Document(source);
tmp.remove("_id");
Document intermediate = new Document(source);
intermediate.remove("_id");
if (tmp.size() == 1) {
return getPotentiallyConvertedSimpleTypeValue(converter, tmp.values().iterator().next(), targetType);
if (intermediate.size() == 1) {
return getPotentiallyConvertedSimpleTypeValue(converter, intermediate.values().iterator().next(), targetType);
}
for (Map.Entry<String, Object> entry : tmp.entrySet()) {
for (Map.Entry<String, Object> entry : intermediate.entrySet()) {
if (entry != null && ClassUtils.isAssignable(targetType, entry.getValue().getClass())) {
return (T) entry.getValue();
return targetType.cast(entry.getValue());
}
}
@@ -192,14 +191,15 @@ class AggregationUtils {
}
@Nullable
private static <T> T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, Object value,
@SuppressWarnings("unchecked")
private static <T> T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value,
Class<T> targetType) {
if (value == null) {
return (T) value;
return null;
}
if (!converter.getConversionService().canConvert(value.getClass(), targetType)) {
if (ClassUtils.isAssignableValue(targetType, value)) {
return (T) value;
}

View File

@@ -168,15 +168,12 @@ public class MongoQueryMethod extends QueryMethod {
return this.metadata;
}
/**
* Get the declared {@link org.springframework.data.repository.Repository} domain type.
*
* @return the domain type declared at repository level.
* @see QueryMethod#getDomainClass()
* @since 2.2
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryMethod#getDomainClass()
*/
Class<?> getRepositoryDomainType() {
return getDomainClass();
protected Class<?> getDomainClass() {
return super.getDomainClass();
}
/*
@@ -317,7 +314,7 @@ public class MongoQueryMethod extends QueryMethod {
* @since 2.1
*/
public boolean hasAnnotatedSort() {
return lookupQueryAnnotation().map(it -> !it.sort().isEmpty()).orElse(false);
return lookupQueryAnnotation().map(Query::sort).filter(StringUtils::hasText).isPresent();
}
/**
@@ -338,13 +335,18 @@ public class MongoQueryMethod extends QueryMethod {
* Check if the query method is decorated with an non empty {@link Query#collation()} or or
* {@link Aggregation#collation()}.
*
* @return true if method annotated with {@link Query} or {@link Aggregation} having an non empty collation attribute.
* @return true if method annotated with {@link Query} or {@link Aggregation} having a non-empty collation attribute.
* @since 2.2
*/
public boolean hasAnnotatedCollation() {
return lookupQueryAnnotation().map(it -> !it.collation().isEmpty())
.orElseGet(() -> lookupAggregationAnnotation().map(it -> !it.collation().isEmpty()).orElse(false));
Optional<String> optionalCollation = lookupQueryAnnotation().map(Query::collation);
if (!optionalCollation.isPresent()) {
optionalCollation = lookupAggregationAnnotation().map(Aggregation::collation);
}
return optionalCollation.filter(StringUtils::hasText).isPresent();
}
/**
@@ -374,15 +376,16 @@ public class MongoQueryMethod extends QueryMethod {
}
/**
* Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found
* nor the attribute was specified.
* Returns the aggregation pipeline declared in a {@link Aggregation} annotation.
*
* @return
* @return the aggregation pipeline.
* @throws IllegalStateException if method not annotated with {@link Aggregation}. Make sure to check
* {@link #hasAnnotatedAggregation()} first.
* @since 2.2
*/
@Nullable
public String[] getAnnotatedAggregation() {
return findAnnotatedAggregation().orElse(null);
return findAnnotatedAggregation().orElseThrow(() -> new IllegalStateException(
"Expected to find @Aggregation annotation but did not. Make sure to check hasAnnotatedAggregation() before."));
}
private Optional<String[]> findAnnotatedAggregation() {

View File

@@ -20,6 +20,7 @@ import reactor.core.publisher.Flux;
import java.util.List;
import org.bson.Document;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
@@ -27,7 +28,6 @@ import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
@@ -39,6 +39,7 @@ import org.springframework.util.ClassUtils;
* {@link AggregationOperation aggregation} pipeline to actually execute.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.2
*/
public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
@@ -74,10 +75,10 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
protected Object doExecute(ReactiveMongoQueryMethod method, ResultProcessor processor,
ConvertingParameterAccessor accessor, Class<?> typeToRead) {
Class<?> sourceType = method.getRepositoryDomainType();
Class<?> sourceType = method.getDomainClass();
Class<?> targetType = typeToRead;
List<AggregationOperation> pipeline = computePipeline(method, accessor);
List<AggregationOperation> pipeline = computePipeline(accessor);
AggregationUtils.appendSortIfPresent(pipeline, accessor, typeToRead);
AggregationUtils.appendLimitAndOffsetIfPresent(pipeline, accessor);
@@ -94,17 +95,21 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
Flux<?> flux = reactiveMongoOperations.aggregate(aggregation, targetType);
if (isSimpleReturnType && !isRawReturnType) {
return flux.map(it -> AggregationUtils.extractSimpleTypeResult((Document) it, typeToRead, mongoConverter));
flux = flux.map(it -> AggregationUtils.extractSimpleTypeResult((Document) it, typeToRead, mongoConverter));
}
return flux;
if (method.isCollectionQuery()) {
return flux;
} else {
return flux.next();
}
}
private boolean isSimpleReturnType(Class<?> targetType) {
return MongoSimpleTypes.HOLDER.isSimpleType(targetType);
}
List<AggregationOperation> computePipeline(MongoQueryMethod method, ConvertingParameterAccessor accessor) {
List<AggregationOperation> computePipeline(ConvertingParameterAccessor accessor) {
return AggregationUtils.computePipeline(getQueryMethod(), accessor, expressionParser, evaluationContextProvider);
}
@@ -122,7 +127,7 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
*/
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
return new BasicQuery("{}");
throw new UnsupportedOperationException("No query support for aggregation");
}
/*

View File

@@ -19,6 +19,7 @@ import java.util.List;
import java.util.stream.Collectors;
import org.bson.Document;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
@@ -27,7 +28,6 @@ import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
@@ -71,7 +71,7 @@ public class StringBasedAggregation extends AbstractMongoQuery {
protected Object doExecute(MongoQueryMethod method, ResultProcessor resultProcessor,
ConvertingParameterAccessor accessor, Class<?> typeToRead) {
Class<?> sourceType = method.getRepositoryDomainType();
Class<?> sourceType = method.getDomainClass();
Class<?> targetType = typeToRead;
List<AggregationOperation> pipeline = computePipeline(method, accessor);
@@ -84,7 +84,7 @@ public class StringBasedAggregation extends AbstractMongoQuery {
if (isSimpleReturnType) {
targetType = Document.class;
} else if (isRawAggregationResult) {
targetType = method.getReturnType().getActualType().getComponentType().getType();
targetType = method.getReturnType().getRequiredActualType().getRequiredComponentType().getType();
}
AggregationOptions options = computeOptions(method, accessor);
@@ -108,13 +108,11 @@ public class StringBasedAggregation extends AbstractMongoQuery {
return result.getMappedResults();
}
if (isSimpleReturnType) {
Object uniqueResult = result.getUniqueMappedResult();
return AggregationUtils.extractSimpleTypeResult((Document) result.getUniqueMappedResult(), typeToRead,
mongoConverter);
}
return result.getUniqueMappedResult();
return isSimpleReturnType
? AggregationUtils.extractSimpleTypeResult((Document) uniqueResult, typeToRead, mongoConverter)
: uniqueResult;
}
private boolean isSimpleReturnType(Class<?> targetType) {
@@ -139,7 +137,7 @@ public class StringBasedAggregation extends AbstractMongoQuery {
*/
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
return new BasicQuery("{}");
throw new UnsupportedOperationException("No query support for aggregation");
}
/*

View File

@@ -1347,7 +1347,7 @@ public abstract class AbstractPersonRepositoryIntegrationTests {
@Test // DATAMONGO-2153
public void annotatedAggregationWithSingleSimpleResult() {
assertThat(repository.sumAge()).isInstanceOf(Long.class).isEqualTo(245L);
assertThat(repository.sumAge()).isEqualTo(245);
}
@Test // DATAMONGO-2153

View File

@@ -17,16 +17,29 @@ package org.springframework.data.mongodb.repository;
import lombok.Value;
import java.util.Collections;
import java.util.List;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
@Value
public class PersonAggregate {
class PersonAggregate {
@Id private String lastname;
private List<String> names;
@PersistenceConstructor
public PersonAggregate(String lastname, List<String> names) {
this.lastname = lastname;
this.names = names;
}
public PersonAggregate(String lastname, String name) {
this(lastname, Collections.singletonList(name));
}
}

View File

@@ -381,7 +381,7 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
List<PersonAggregate> groupByLastnameAnd(String property, Pageable page);
@Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }")
Long sumAge();
int sumAge();
@Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }")
AggregationResults<org.bson.Document> sumAgeAndReturnAggregationResultWrapper();

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.data.mongodb.repository;
import static org.assertj.core.api.Assertions.offset;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.domain.Sort.Direction.*;
import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
@@ -27,7 +27,6 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
@@ -37,6 +36,7 @@ import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@@ -455,11 +455,11 @@ public class ReactiveMongoRepositoryTests {
.as(StepVerifier::create) //
.assertNext(actual -> {
assertThat(actual) //
.contains(new PersonAggregate("Lessard", Collections.singletonList("Stefan"))) //
.contains(new PersonAggregate("Keys", Collections.singletonList("Alicia"))) //
.contains(new PersonAggregate("Tinsley", Collections.singletonList("Boyd"))) //
.contains(new PersonAggregate("Beauford", Collections.singletonList("Carter"))) //
.contains(new PersonAggregate("Moore", Collections.singletonList("Leroi"))) //
.contains(new PersonAggregate("Lessard", "Stefan")) //
.contains(new PersonAggregate("Keys", "Alicia")) //
.contains(new PersonAggregate("Tinsley", "Boyd")) //
.contains(new PersonAggregate("Beauford", "Carter")) //
.contains(new PersonAggregate("Moore", "Leroi")) //
.contains(new PersonAggregate("Matthews", Arrays.asList("Dave", "Oliver August")));
}).verifyComplete();
}
@@ -473,12 +473,12 @@ public class ReactiveMongoRepositoryTests {
.assertNext(actual -> {
assertThat(actual) //
.containsSequence( //
new PersonAggregate("Beauford", Collections.singletonList("Carter")), //
new PersonAggregate("Keys", Collections.singletonList("Alicia")), //
new PersonAggregate("Lessard", Collections.singletonList("Stefan")), //
new PersonAggregate("Beauford", "Carter"), //
new PersonAggregate("Keys", "Alicia"), //
new PersonAggregate("Lessard", "Stefan"), //
new PersonAggregate("Matthews", Arrays.asList("Dave", "Oliver August")), //
new PersonAggregate("Moore", Collections.singletonList("Leroi")), //
new PersonAggregate("Tinsley", Collections.singletonList("Boyd")));
new PersonAggregate("Moore", "Leroi"), //
new PersonAggregate("Tinsley", "Boyd"));
}) //
.verifyComplete();
}
@@ -492,7 +492,7 @@ public class ReactiveMongoRepositoryTests {
.assertNext(actual -> {
assertThat(actual) //
.containsExactly( //
new PersonAggregate("Lessard", Collections.singletonList("Stefan")), //
new PersonAggregate("Lessard", "Stefan"), //
new PersonAggregate("Matthews", Arrays.asList("Dave", "Oliver August")));
}) //
.verifyComplete();

View File

@@ -21,7 +21,7 @@ import lombok.Value;
* @author Christoph Strobl
*/
@Value
public class SumAge {
class SumAge {
private Long total;
}

View File

@@ -16,7 +16,7 @@
package org.springframework.data.mongodb.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import lombok.Value;
@@ -34,6 +34,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
@@ -55,8 +56,11 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextPro
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* Unit tests for {@link ReactiveStringBasedAggregation}.
*
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
@@ -158,18 +162,12 @@ public class ReactiveStringBasedAggregationUnitTests {
private ReactiveStringBasedAggregation createAggregationForMethod(String name, Class<?>... parameters) {
try {
Method method = SampleRepository.class.getMethod(name, parameters);
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method,
new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext());
return new ReactiveStringBasedAggregation(queryMethod, operations, PARSER,
QueryMethodEvaluationContextProvider.DEFAULT);
} catch (Exception e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
Method method = ClassUtils.getMethod(SampleRepository.class, name, parameters);
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method,
new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext());
return new ReactiveStringBasedAggregation(queryMethod, operations, PARSER,
QueryMethodEvaluationContextProvider.DEFAULT);
}
private List<Document> pipelineOf(AggregationInvocation invocation) {
@@ -222,8 +220,8 @@ public class ReactiveStringBasedAggregationUnitTests {
@Value
static class AggregationInvocation {
final TypedAggregation<?> aggregation;
final Class<?> targetType;
final Object result;
TypedAggregation<?> aggregation;
Class<?> targetType;
Object result;
}
}

View File

@@ -16,7 +16,7 @@
package org.springframework.data.mongodb.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import lombok.Value;
@@ -25,6 +25,7 @@ import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.IntFunction;
import org.bson.Document;
import org.junit.Before;
@@ -33,6 +34,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.MongoOperations;
@@ -55,8 +57,11 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* Unit tests for {@link StringBasedAggregation}.
*
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
@@ -176,7 +181,7 @@ public class StringBasedAggregationUnitTests {
private AggregationInvocation executeAggregation(String name, Object... args) {
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(size -> new Class<?>[size]);
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
StringBasedAggregation aggregation = createAggregationForMethod(name, argTypes);
ArgumentCaptor<TypedAggregation> aggregationCaptor = ArgumentCaptor.forClass(TypedAggregation.class);
@@ -191,17 +196,11 @@ public class StringBasedAggregationUnitTests {
private StringBasedAggregation createAggregationForMethod(String name, Class<?>... parameters) {
try {
Method method = SampleRepository.class.getMethod(name, parameters);
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class),
factory, converter.getMappingContext());
return new StringBasedAggregation(queryMethod, operations, PARSER, QueryMethodEvaluationContextProvider.DEFAULT);
} catch (Exception e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
Method method = ClassUtils.getMethod(SampleRepository.class, name, parameters);
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class),
factory, converter.getMappingContext());
return new StringBasedAggregation(queryMethod, operations, PARSER, QueryMethodEvaluationContextProvider.DEFAULT);
}
private List<Document> pipelineOf(AggregationInvocation invocation) {
@@ -266,8 +265,8 @@ public class StringBasedAggregationUnitTests {
@Value
static class AggregationInvocation {
final TypedAggregation<?> aggregation;
final Class<?> targetType;
final Object result;
TypedAggregation<?> aggregation;
Class<?> targetType;
Object result;
}
}

View File

@@ -1,11 +1,9 @@
[[mongodb.repositories.queries.aggregation]]
=== Aggregation Repository Methods
The repository layer offers means interact with <<mongo.aggregation, the aggregation framework>> via annotated repository
finder methods. Similar to the <<mongodb.repositories.queries.json-based, JSON based queries>> a pipeline can be defined
via the `org.springframework.data.mongodb.repository.Aggregation` annotation. The definition may contain simple placeholders
like `?0` as well as https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#expressions[SpEL expressions]
`?#{ ... }`.
The repository layer offers means to interact with <<mongo.aggregation, the aggregation framework>> via annotated repository query methods.
Similar to the <<mongodb.repositories.queries.json-based, JSON based queries>>, you can define a pipeline using the `org.springframework.data.mongodb.repository.Aggregation` annotation.
The definition may contain simple placeholders like `?0` as well as https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#expressions[SpEL expressions] `?#{ … }`.
.Aggregating Repository Method
====
@@ -13,33 +11,37 @@ like `?0` as well as https://docs.spring.io/spring/docs/{springVersion}/spring-f
----
public interface PersonRepository extends CrudReppsitory<Person, String> {
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $?0 } } }")
List<PersonAggregate> groupByLastnameAnd(String property); <1>
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
List<PersonAggregate> groupByLastnameAndFirstnames(Sort sort); <2>
List<PersonAggregate> groupByLastnameAndFirstnames(); <1>
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
List<PersonAggregate> groupByLastnameAndFirstnames(Sort sort); <2>
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $?0 } } }")
List<PersonAggregate> groupByLastnameAnd(String property, Pageable page); <3>
List<PersonAggregate> groupByLastnameAnd(String property); <3>
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $?0 } } }")
List<PersonAggregate> groupByLastnameAnd(String property, Pageable page); <4>
@Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
SumValue sumAgeUsingValueWrapper(); <4>
SumValue sumAgeUsingValueWrapper(); <5>
@Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
Long sumAge(); <5>
Long sumAge(); <6>
@Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
AggregationResults<SumValue> sumAgeRaw(); <6>
AggregationResults<SumValue> sumAgeRaw(); <7>
@Aggregation("{ '$project': { '_id' : '$lastname' } }")
List<String> findAllLastnames(); <7>
List<String> findAllLastnames(); <8>
}
----
[source,java]
----
public class PersonAggregate {
private @Id String lastname; <2>
private @Id String lastname; <2>
private List<String> names;
public PersonAggregate(String lastname, List<String> names) {
@@ -51,7 +53,7 @@ public class PersonAggregate {
public class SumValue {
private final Long total; <4> <6>
private final Long total; <5> <7>
public SumValue(Long total) {
// ...
@@ -60,30 +62,28 @@ public class SumValue {
// Getter omitted
}
----
<1> Replace `?0` with the given value for `property`.
<2> If `Sort` argument is present, `$sort` is added at the pipelines tail so that it only affects the order of the final results
after having passed all other aggregation stages. Therefore the `Sort` properties are mapped against the methods return type
`PersonAggregate` which turns `Sort.by("lastname")` into `{ $sort : { '_id', 1 } }` because `PersonAggregate.lastname` is
annotated with `@Id`.
<3> `$skip`, `$limit` and `$sort` can be passed on via a `Pageable` argument. Same as in 2., the operators are applied at
the pipelines tail.
<4> Map the result of an aggregation returning a single `Document` to an instance of a desired `SumValue` target type.
<5> Aggregations resulting in single document holding just an accumulation result like eg. `$sum` can be extracted directly from
the result `Document`. To gain more control one might consider `AggregationResult` as the methods return type as shown in 4. or 6.
<6> Obtain the raw `AggregationResults` mapped to the generic target wrapper type `SumValue` or `org.bson.Document`.
<7> Like in (5) a single value can be directly obtained from mutliple result ``Document``s.
<1> Aggregation pipeline to group first names by `lastname` in the `Person` collection returning these as `PersonAggregate`.
<2> If `Sort` argument is present, `$sort` is appended after the declared pipeline stages so that it only affects the order of the final results after having passed all other aggregation stages.
Therefore, the `Sort` properties are mapped against the methods return type `PersonAggregate` which turns `Sort.by("lastname")` into `{ $sort : { '_id', 1 } }` because `PersonAggregate.lastname` is annotated with `@Id`.
<3> Replaces `?0` with the given value for `property` for a dynamic aggregation pipeline.
<4> `$skip`, `$limit` and `$sort` can be passed on via a `Pageable` argument. Same as in <2>, the operators are appended to the pipeline definition.
<5> Map the result of an aggregation returning a single `Document` to an instance of a desired `SumValue` target type.
<6> Aggregations resulting in single document holding just an accumulation result like eg. `$sum` can be extracted directly from the result `Document`.
To gain more control, you might consider `AggregationResult` as method return type as shown in <7>.
<7> Obtain the raw `AggregationResults` mapped to the generic target wrapper type `SumValue` or `org.bson.Document`.
<8> Like in <6>, a single value can be directly obtained from multiple result ``Document``s.
====
TIP: `@Aggregation` can also be used with <<mongo.reactive.repositories, Reactive Repositories>>.
TIP: You can use `@Aggregation` also with <<mongo.reactive.repositories, Reactive Repositories>>.
[NOTE]
====
Obtaining simple type single results inspects the returned `Document` and checks for the following
Simple-type single-result inspects the returned `Document` and checks for the following:
. Only one entry in the document, return it.
. Two entries, one is the `_id` value. Return the other.
. Return for the first value assignable to the return type.
. Throw an execption if none of the above applied.
. Throw an exception if none of the above is applicable.
====
WARNING: The `Page` return type is not supported for repository methods using `@Aggregation`. However you can use a