diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java index fb721c3db..605fa6066 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java @@ -67,7 +67,9 @@ class AggregationUtil { AggregationOperationContext createAggregationContext(Aggregation aggregation, @Nullable Class inputType) { - if (aggregation.getOptions().getDomainTypeMapping() == DomainTypeMapping.NONE) { + DomainTypeMapping domainTypeMapping = aggregation.getOptions().getDomainTypeMapping(); + + if (domainTypeMapping == DomainTypeMapping.NONE) { return Aggregation.DEFAULT_CONTEXT; } @@ -77,7 +79,7 @@ class AggregationUtil { return untypedMappingContext.get(); } - if (aggregation.getOptions().getDomainTypeMapping() == DomainTypeMapping.STRICT + if (domainTypeMapping == DomainTypeMapping.STRICT && !aggregation.getPipeline().containsUnionWith()) { return new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper); } @@ -85,8 +87,8 @@ class AggregationUtil { return new RelaxedTypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper); } - inputType = ((TypedAggregation) aggregation).getInputType(); - if (aggregation.getOptions().getDomainTypeMapping() == DomainTypeMapping.STRICT + inputType = ((TypedAggregation) aggregation).getInputType(); + if (domainTypeMapping == DomainTypeMapping.STRICT && !aggregation.getPipeline().containsUnionWith()) { return new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper); } @@ -130,53 +132,6 @@ class AggregationUtil { return command; } - /** - * Create a {@code $count} aggregation for {@link Query} and optionally a {@link Class entity class}. - * - * @param query must not be {@literal null}. - * @param entityClass can be {@literal null} if the {@link Query} object is empty. - * @return the {@link Aggregation} pipeline definition to run a {@code $count} aggregation. - */ - Aggregation createCountAggregation(Query query, @Nullable Class entityClass) { - - List pipeline = computeCountAggregationPipeline(query, entityClass); - - Aggregation aggregation = entityClass != null ? Aggregation.newAggregation(entityClass, pipeline) - : Aggregation.newAggregation(pipeline); - aggregation.withOptions(AggregationOptions.builder().collation(query.getCollation().orElse(null)).build()); - - return aggregation; - } - - private List computeCountAggregationPipeline(Query query, @Nullable Class entityType) { - - CountOperation count = Aggregation.count().as("totalEntityCount"); - if (query.getQueryObject().isEmpty()) { - return Collections.singletonList(count); - } - - Assert.notNull(entityType, "Entity type must not be null!"); - - Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), - mappingContext.getPersistentEntity(entityType)); - - CriteriaDefinition criteria = new CriteriaDefinition() { - - @Override - public Document getCriteriaObject() { - return mappedQuery; - } - - @Nullable - @Override - public String getKey() { - return null; - } - }; - - return Arrays.asList(Aggregation.match(criteria), count); - } - private List mapAggregationPipeline(List pipeline) { return pipeline.stream().map(val -> queryMapper.getMappedObject(val, Optional.empty())) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 16a8b1998..f43bec7a6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -55,7 +55,7 @@ import org.springframework.data.mongodb.SessionSynchronization; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; import org.springframework.data.mongodb.core.DefaultBulkOperations.BulkOperationContext; import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity; -import org.springframework.data.mongodb.core.QueryOperations.AggregateContext; +import org.springframework.data.mongodb.core.QueryOperations.AggregationDefinition; import org.springframework.data.mongodb.core.QueryOperations.CountContext; import org.springframework.data.mongodb.core.QueryOperations.DeleteContext; import org.springframework.data.mongodb.core.QueryOperations.DistinctQueryContext; @@ -1989,7 +1989,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, public AggregationResults aggregate(Aggregation aggregation, Class inputType, Class outputType) { return aggregate(aggregation, getCollectionName(inputType), outputType, - queryOperations.createAggregationContext(aggregation, inputType).getAggregationOperationContext()); + queryOperations.createAggregation(aggregation, inputType).getAggregationOperationContext()); } /* (non-Javadoc) @@ -2096,11 +2096,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(aggregation, "Aggregation pipeline must not be null!"); Assert.notNull(outputType, "Output type must not be null!"); - return doAggregate(aggregation, collectionName, outputType, queryOperations.createAggregationContext(aggregation, context)); + return doAggregate(aggregation, collectionName, outputType, + queryOperations.createAggregation(aggregation, context)); } private AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, - AggregateContext context) { + AggregationDefinition context) { return doAggregate(aggregation, collectionName, outputType, context.getAggregationOperationContext()); } @@ -2189,10 +2190,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(outputType, "Output type must not be null!"); Assert.isTrue(!aggregation.getOptions().isExplain(), "Can't use explain option with streaming!"); - AggregateContext aggregateContext = queryOperations.createAggregationContext(aggregation, context); + AggregationDefinition aggregationDefinition = queryOperations.createAggregation(aggregation, context); AggregationOptions options = aggregation.getOptions(); - List pipeline = aggregateContext.getAggregationPipeline(); + List pipeline = aggregationDefinition.getAggregationPipeline(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Streaming aggregation: {} in collection {}", serializeToJsonSafely(pipeline), collectionName); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java index ec120047e..89e7d2978 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java @@ -200,33 +200,33 @@ class QueryOperations { } /** - * Create a new {@link AggregateContext} for the given {@link Aggregation}. + * Create a new {@link AggregationDefinition} for the given {@link Aggregation}. * * @param aggregation must not be {@literal null}. * @param inputType fallback mapping type in case of untyped aggregation. Can be {@literal null}. - * @return new instance of {@link AggregateContext}. + * @return new instance of {@link AggregationDefinition}. * @since 3.2 */ - AggregateContext createAggregationContext(Aggregation aggregation, @Nullable Class inputType) { - return new AggregateContext(aggregation, inputType); + AggregationDefinition createAggregation(Aggregation aggregation, @Nullable Class inputType) { + return new AggregationDefinition(aggregation, inputType); } /** - * Create a new {@link AggregateContext} for the given {@link Aggregation}. + * Create a new {@link AggregationDefinition} for the given {@link Aggregation}. * * @param aggregation must not be {@literal null}. * @param aggregationOperationContext the {@link AggregationOperationContext} to use. Can be {@literal null}. - * @return new instance of {@link AggregateContext}. + * @return new instance of {@link AggregationDefinition}. * @since 3.2 */ - AggregateContext createAggregationContext(Aggregation aggregation, + AggregationDefinition createAggregation(Aggregation aggregation, @Nullable AggregationOperationContext aggregationOperationContext) { - return new AggregateContext(aggregation, aggregationOperationContext); + return new AggregationDefinition(aggregation, aggregationOperationContext); } /** * {@link QueryContext} encapsulates common tasks required to convert a {@link Query} into its MongoDB document - * representation, mapping fieldnames, as well as determinging and applying {@link Collation collations}. + * representation, mapping field names, as well as determining and applying {@link Collation collations}. * * @author Christoph Strobl */ @@ -235,7 +235,7 @@ class QueryOperations { private final Query query; /** - * Create new a {@link QueryContext} instance from the given {@literal query} (can be eihter a {@link Query} or a + * Create new a {@link QueryContext} instance from the given {@literal query} (can be either a {@link Query} or a * plain {@link Document}. * * @param query can be {@literal null}. @@ -305,7 +305,7 @@ class QueryOperations { mappingContext.getRequiredPersistentEntity(targetType)); } - if (entity != null && entity.hasTextScoreProperty() && !query.getQueryObject().containsKey("$text")) { + if (entity.hasTextScoreProperty() && !query.getQueryObject().containsKey("$text")) { mappedFields.remove(entity.getTextScoreProperty().getFieldName()); } @@ -793,19 +793,19 @@ class QueryOperations { } /** - * A context class that encapsulates common tasks required when running {@literal aggregations}. + * A value object that encapsulates common tasks required when running {@literal aggregations}. * * @since 3.2 */ - class AggregateContext { + class AggregationDefinition { - private Aggregation aggregation; - private Lazy aggregationOperationContext; - private Lazy> pipeline; - private @Nullable Class inputType; + private final Aggregation aggregation; + private final Lazy aggregationOperationContext; + private final Lazy> pipeline; + private final @Nullable Class inputType; /** - * Creates new instance of {@link AggregateContext} extracting the input type from either the + * Creates new instance of {@link AggregationDefinition} extracting the input type from either the * {@link org.springframework.data.mongodb.core.aggregation.Aggregation} in case of a {@link TypedAggregation} or * the given {@literal aggregationOperationContext} if present.
* Creates a new {@link AggregationOperationContext} if none given, based on the {@link Aggregation} input type and @@ -815,21 +815,25 @@ class QueryOperations { * @param aggregation the source aggregation. * @param aggregationOperationContext can be {@literal null}. */ - AggregateContext(Aggregation aggregation, @Nullable AggregationOperationContext aggregationOperationContext) { + AggregationDefinition(Aggregation aggregation, @Nullable AggregationOperationContext aggregationOperationContext) { this.aggregation = aggregation; + if (aggregation instanceof TypedAggregation) { - this.inputType = ((TypedAggregation) aggregation).getInputType(); + this.inputType = ((TypedAggregation) aggregation).getInputType(); } else if (aggregationOperationContext instanceof TypeBasedAggregationOperationContext) { this.inputType = ((TypeBasedAggregationOperationContext) aggregationOperationContext).getType(); + } else { + this.inputType = null; } + this.aggregationOperationContext = Lazy.of(() -> aggregationOperationContext != null ? aggregationOperationContext : aggregationUtil.createAggregationContext(aggregation, getInputType())); this.pipeline = Lazy.of(() -> aggregationUtil.createPipeline(this.aggregation, getAggregationOperationContext())); } /** - * Creates new instance of {@link AggregateContext} extracting the input type from either the + * Creates new instance of {@link AggregationDefinition} extracting the input type from either the * {@link org.springframework.data.mongodb.core.aggregation.Aggregation} in case of a {@link TypedAggregation} or * the given {@literal aggregationOperationContext} if present.
* Creates a new {@link AggregationOperationContext} based on the {@link Aggregation} input type and the desired @@ -839,12 +843,12 @@ class QueryOperations { * @param aggregation the source aggregation. * @param inputType can be {@literal null}. */ - AggregateContext(Aggregation aggregation, @Nullable Class inputType) { + AggregationDefinition(Aggregation aggregation, @Nullable Class inputType) { this.aggregation = aggregation; if (aggregation instanceof TypedAggregation) { - this.inputType = ((TypedAggregation) aggregation).getInputType(); + this.inputType = ((TypedAggregation) aggregation).getInputType(); } else { this.inputType = inputType; } @@ -887,9 +891,5 @@ class QueryOperations { Class getInputType() { return inputType; } - - Document getAggregationCommand(String collectionName) { - return aggregationUtil.createCommand(collectionName, aggregation, getAggregationOperationContext()); - } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 62673dbbf..a73ff5ca7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -17,7 +17,7 @@ package org.springframework.data.mongodb.core; import static org.springframework.data.mongodb.core.query.SerializationUtils.*; -import org.springframework.data.mongodb.core.QueryOperations.AggregateContext; +import org.springframework.data.mongodb.core.QueryOperations.AggregationDefinition; import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -986,7 +986,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati AggregationOptions options = aggregation.getOptions(); Assert.isTrue(!options.isExplain(), "Cannot use explain option with streaming!"); - AggregateContext ctx = queryOperations.createAggregationContext(aggregation, inputType); + AggregationDefinition ctx = queryOperations.createAggregation(aggregation, inputType); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Streaming aggregation: {} in collection {}", serializeToJsonSafely(ctx.getAggregationPipeline()), collectionName); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java index 4ef733506..e1afc8494 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java @@ -487,7 +487,7 @@ public class AggregationOptions { /** * Apply a strict domain type mapping considering {@link org.springframework.data.mongodb.core.mapping.Field} - * annotations throwing errors for non existing, but referenced fields. + * annotations throwing errors for non-existent, but referenced fields. * * @return this. * @since 3.2 @@ -512,7 +512,7 @@ public class AggregationOptions { } /** - * Apply no domain type mapping at all taking the pipeline as is. + * Apply no domain type mapping at all taking the pipeline as-is. * * @return this. * @since 3.2 @@ -568,15 +568,17 @@ public class AggregationOptions { public enum DomainTypeMapping { /** - * Mapping throws errors for non existing, but referenced fields. + * Mapping throws errors for non-existent, but referenced fields. */ STRICT, + /** - * Fields that do not exist in the model are treated as is. + * Fields that do not exist in the model are treated as-is. */ RELAXED, + /** - * Do not attempt to map fields against the model and treat the entire pipeline as is. + * Do not attempt to map fields against the model and treat the entire pipeline as-is. */ NONE } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryOperationsUnitTests.java index 6a49adf26..e463daa77 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryOperationsUnitTests.java @@ -25,7 +25,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.MongoDatabaseFactory; -import org.springframework.data.mongodb.core.QueryOperations.AggregateContext; +import org.springframework.data.mongodb.core.QueryOperations.AggregationDefinition; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext; @@ -35,8 +35,9 @@ import org.springframework.data.mongodb.core.convert.UpdateMapper; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; /** + * Unit tests for {@link QueryOperations}. + * * @author Christoph Strobl - * @since 2021/01 */ @ExtendWith(MockitoExtension.class) class QueryOperationsUnitTests { @@ -66,7 +67,7 @@ class QueryOperationsUnitTests { void createAggregationContextUsesRelaxedOneForUntypedAggregationsWhenNoInputTypeProvided() { Aggregation aggregation = Aggregation.newAggregation(Aggregation.project("name")); - AggregateContext ctx = queryOperations.createAggregationContext(aggregation, (Class) null); + AggregationDefinition ctx = queryOperations.createAggregation(aggregation, (Class) null); assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class); } @@ -75,7 +76,7 @@ class QueryOperationsUnitTests { void createAggregationContextUsesRelaxedOneForTypedAggregationsWhenNoInputTypeProvided() { Aggregation aggregation = Aggregation.newAggregation(Person.class, Aggregation.project("name")); - AggregateContext ctx = queryOperations.createAggregationContext(aggregation, (Class) null); + AggregationDefinition ctx = queryOperations.createAggregation(aggregation, (Class) null); assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class); } @@ -84,7 +85,7 @@ class QueryOperationsUnitTests { void createAggregationContextUsesRelaxedOneForUntypedAggregationsWhenInputTypeProvided() { Aggregation aggregation = Aggregation.newAggregation(Aggregation.project("name")); - AggregateContext ctx = queryOperations.createAggregationContext(aggregation, Person.class); + AggregationDefinition ctx = queryOperations.createAggregation(aggregation, Person.class); assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class); } @@ -93,7 +94,7 @@ class QueryOperationsUnitTests { void createAggregationContextUsesDefaultIfNoMappingDesired() { Aggregation aggregation = Aggregation.newAggregation(Aggregation.project("name")).withOptions(NO_MAPPING); - AggregateContext ctx = queryOperations.createAggregationContext(aggregation, Person.class); + AggregationDefinition ctx = queryOperations.createAggregation(aggregation, Person.class); assertThat(ctx.getAggregationOperationContext()).isEqualTo(Aggregation.DEFAULT_CONTEXT); } @@ -103,7 +104,7 @@ class QueryOperationsUnitTests { Aggregation aggregation = Aggregation.newAggregation(Person.class, Aggregation.project("name")) .withOptions(STRICT_MAPPING); - AggregateContext ctx = queryOperations.createAggregationContext(aggregation, (Class) null); + AggregationDefinition ctx = queryOperations.createAggregation(aggregation, (Class) null); assertThat(ctx.getAggregationOperationContext()).isInstanceOf(TypeBasedAggregationOperationContext.class); } diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 7c25cebd4..71a82f044 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -2502,7 +2502,7 @@ For further information, see the full https://docs.mongodb.org/manual/aggregatio [[mongo.aggregation.basic-concepts]] === Basic Concepts -The Aggregation Framework support in Spring Data MongoDB is based on the following key abstractions: `Aggregation`, `AggregationOperation`, and `AggregationResults`. +The Aggregation Framework support in Spring Data MongoDB is based on the following key abstractions: `Aggregation`, `AggregationDefinition`, and `AggregationResults`. * `Aggregation` + @@ -2517,12 +2517,12 @@ A `TypedAggregation`, just like an `Aggregation`, holds the instructions of the At runtime, field references get checked against the given input type, considering potential `@Field` annotations. [NOTE] ==== -Changed in 3.2 referencing nonexistent properties does no longer raise errors. To restore the previous behaviour use the `strictMapping` option of `AggregationOptions`. +Changed in 3.2 referencing none-xistent properties does no longer raise errors. To restore the previous behaviour use the `strictMapping` option of `AggregationOptions`. ==== + -* `AggregationOperation` +* `AggregationDefinition` + -An `AggregationOperation` represents a MongoDB aggregation pipeline operation and describes the processing that should be performed in this aggregation step. Although you could manually create an `AggregationOperation`, we recommend using the static factory methods provided by the `Aggregate` class to construct an `AggregateOperation`. +An `AggregationDefinition` represents a MongoDB aggregation pipeline operation and describes the processing that should be performed in this aggregation step. Although you could manually create an `AggregationDefinition`, we recommend using the static factory methods provided by the `Aggregate` class to construct an `AggregateOperation`. + * `AggregationResults` +