From f456851791126ee5bfd76e5ded0c1c2cc73509dc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 16 May 2019 16:21:00 +0200 Subject: [PATCH] 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. --- .../data/mongodb/repository/Aggregation.java | 14 ++--- .../repository/query/AggregationUtils.java | 24 ++++---- .../repository/query/MongoQueryMethod.java | 37 ++++++------ .../query/ReactiveStringBasedAggregation.java | 19 +++--- .../query/StringBasedAggregation.java | 18 +++--- ...tractPersonRepositoryIntegrationTests.java | 2 +- .../mongodb/repository/PersonAggregate.java | 15 ++++- .../mongodb/repository/PersonRepository.java | 2 +- .../ReactiveMongoRepositoryTests.java | 26 ++++---- .../data/mongodb/repository/SumAge.java | 2 +- ...activeStringBasedAggregationUnitTests.java | 30 +++++----- .../StringBasedAggregationUnitTests.java | 31 +++++----- .../mongo-repositories-aggregation.adoc | 60 +++++++++---------- 13 files changed, 148 insertions(+), 132 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java index 0c74fea64..eea5b0118 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Aggregation.java @@ -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.
- * 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}.
+ * 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. + *

+ * 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}. + *

* 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 { * * * @return an empty {@link String} by default. - * @since 2.2 */ String collation() default ""; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java index 3fe02a4af..3c71a651e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java @@ -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 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 entry : tmp.entrySet()) { + for (Map.Entry 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 getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, Object value, + @SuppressWarnings("unchecked") + private static T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value, Class targetType) { if (value == null) { - return (T) value; + return null; } - if (!converter.getConversionService().canConvert(value.getClass(), targetType)) { + if (ClassUtils.isAssignableValue(targetType, value)) { return (T) value; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java index 21e375043..a8d244424 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java @@ -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 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 findAnnotatedAggregation() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java index 211e8daa2..c362366c1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java @@ -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 pipeline = computePipeline(method, accessor); + List 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 computePipeline(MongoQueryMethod method, ConvertingParameterAccessor accessor) { + List 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"); } /* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java index ebb33e514..ff6fa84b2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java @@ -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 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"); } /* diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 50e303cc8..444d8e902 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -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 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java index 58931ddd1..7e4ec3337 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java @@ -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 names; + + @PersistenceConstructor + public PersonAggregate(String lastname, List names) { + this.lastname = lastname; + this.names = names; + } + + public PersonAggregate(String lastname, String name) { + this(lastname, Collections.singletonList(name)); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index 59437cfbf..19b445ec5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -381,7 +381,7 @@ public interface PersonRepository extends MongoRepository, Query List 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 sumAgeAndReturnAggregationResultWrapper(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java index 2007f3cee..55406f5eb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java @@ -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(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java index e0b99244d..ce4b41d81 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java @@ -21,7 +21,7 @@ import lombok.Value; * @author Christoph Strobl */ @Value -public class SumAge { +class SumAge { private Long total; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java index cb9a4de1d..23735660e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java @@ -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 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; } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java index e25d91854..44f718648 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java @@ -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 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 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; } } diff --git a/src/main/asciidoc/reference/mongo-repositories-aggregation.adoc b/src/main/asciidoc/reference/mongo-repositories-aggregation.adoc index 70e5e295c..feba576ad 100644 --- a/src/main/asciidoc/reference/mongo-repositories-aggregation.adoc +++ b/src/main/asciidoc/reference/mongo-repositories-aggregation.adoc @@ -1,11 +1,9 @@ [[mongodb.repositories.queries.aggregation]] === Aggregation Repository Methods -The repository layer offers means interact with <> via annotated repository -finder methods. Similar to the <> 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 <> via annotated repository query methods. +Similar to the <>, 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 { - @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $?0 } } }") - List groupByLastnameAnd(String property); <1> @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }") - List groupByLastnameAndFirstnames(Sort sort); <2> + List groupByLastnameAndFirstnames(); <1> + + @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }") + List groupByLastnameAndFirstnames(Sort sort); <2> @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $?0 } } }") - List groupByLastnameAnd(String property, Pageable page); <3> + List groupByLastnameAnd(String property); <3> + + @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $?0 } } }") + List 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 sumAgeRaw(); <6> + AggregationResults sumAgeRaw(); <7> @Aggregation("{ '$project': { '_id' : '$lastname' } }") - List findAllLastnames(); <7> + List findAllLastnames(); <8> } ---- [source,java] ---- public class PersonAggregate { - private @Id String lastname; <2> + private @Id String lastname; <2> private List names; public PersonAggregate(String lastname, List 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 <>. +TIP: You can use `@Aggregation` also with <>. [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