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:
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -21,7 +21,7 @@ import lombok.Value;
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@Value
|
||||
public class SumAge {
|
||||
class SumAge {
|
||||
|
||||
private Long total;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user