diff --git a/pom.xml b/pom.xml
index 10c1adf1bf..7d1121c693 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.2.0-SNAPSHOT
+ 3.2.0-GH-3542-SNAPSHOT
pom
Spring Data MongoDB
diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml
index f0fbb601c8..ecb2010181 100644
--- a/spring-data-mongodb-benchmarks/pom.xml
+++ b/spring-data-mongodb-benchmarks/pom.xml
@@ -7,7 +7,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.2.0-SNAPSHOT
+ 3.2.0-GH-3542-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml
index 1a17321782..c983ef7976 100644
--- a/spring-data-mongodb-distribution/pom.xml
+++ b/spring-data-mongodb-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.2.0-SNAPSHOT
+ 3.2.0-GH-3542-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml
index 0248517caf..14cdbcb318 100644
--- a/spring-data-mongodb/pom.xml
+++ b/spring-data-mongodb/pom.xml
@@ -11,7 +11,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.2.0-SNAPSHOT
+ 3.2.0-GH-3542-SNAPSHOT
../pom.xml
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 adb2e1d8ba..fb721c3db3 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
@@ -27,6 +27,7 @@
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
+import org.springframework.data.mongodb.core.aggregation.AggregationOptions.DomainTypeMapping;
import org.springframework.data.mongodb.core.aggregation.CountOperation;
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
@@ -36,6 +37,7 @@
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -52,41 +54,44 @@ class AggregationUtil {
QueryMapper queryMapper;
MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext;
+ Lazy untypedMappingContext;
AggregationUtil(QueryMapper queryMapper,
MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext) {
this.queryMapper = queryMapper;
this.mappingContext = mappingContext;
+ this.untypedMappingContext = Lazy
+ .of(() -> new RelaxedTypeBasedAggregationOperationContext(Object.class, mappingContext, queryMapper));
}
- /**
- * Prepare the {@link AggregationOperationContext} for a given aggregation by either returning the context itself it
- * is not {@literal null}, create a {@link TypeBasedAggregationOperationContext} if the aggregation contains type
- * information (is a {@link TypedAggregation}) or use the {@link Aggregation#DEFAULT_CONTEXT}.
- *
- * @param aggregation must not be {@literal null}.
- * @param context can be {@literal null}.
- * @return the root {@link AggregationOperationContext} to use.
- */
- AggregationOperationContext prepareAggregationContext(Aggregation aggregation,
- @Nullable AggregationOperationContext context) {
+ AggregationOperationContext createAggregationContext(Aggregation aggregation, @Nullable Class> inputType) {
- if (context != null) {
- return context;
+ if (aggregation.getOptions().getDomainTypeMapping() == DomainTypeMapping.NONE) {
+ return Aggregation.DEFAULT_CONTEXT;
}
if (!(aggregation instanceof TypedAggregation)) {
- return new RelaxedTypeBasedAggregationOperationContext(Object.class, mappingContext, queryMapper);
- }
- Class> inputType = ((TypedAggregation) aggregation).getInputType();
+ if(inputType == null) {
+ return untypedMappingContext.get();
+ }
+
+ if (aggregation.getOptions().getDomainTypeMapping() == DomainTypeMapping.STRICT
+ && !aggregation.getPipeline().containsUnionWith()) {
+ return new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
+ }
- if (aggregation.getPipeline().containsUnionWith()) {
return new RelaxedTypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
}
- return new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
+ inputType = ((TypedAggregation) aggregation).getInputType();
+ if (aggregation.getOptions().getDomainTypeMapping() == DomainTypeMapping.STRICT
+ && !aggregation.getPipeline().containsUnionWith()) {
+ return new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
+ }
+
+ return new RelaxedTypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
}
/**
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 3955d860d8..16a8b19989 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,6 +55,7 @@
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.CountContext;
import org.springframework.data.mongodb.core.QueryOperations.DeleteContext;
import org.springframework.data.mongodb.core.QueryOperations.DistinctQueryContext;
@@ -1988,7 +1989,7 @@ public AggregationResults aggregate(TypedAggregation> aggregation, Stri
public AggregationResults aggregate(Aggregation aggregation, Class> inputType, Class outputType) {
return aggregate(aggregation, getCollectionName(inputType), outputType,
- new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper));
+ queryOperations.createAggregationContext(aggregation, inputType).getAggregationOperationContext());
}
/* (non-Javadoc)
@@ -2095,9 +2096,12 @@ protected AggregationResults aggregate(Aggregation aggregation, String co
Assert.notNull(aggregation, "Aggregation pipeline must not be null!");
Assert.notNull(outputType, "Output type must not be null!");
- AggregationOperationContext contextToUse = new AggregationUtil(queryMapper, mappingContext)
- .prepareAggregationContext(aggregation, context);
- return doAggregate(aggregation, collectionName, outputType, contextToUse);
+ return doAggregate(aggregation, collectionName, outputType, queryOperations.createAggregationContext(aggregation, context));
+ }
+
+ private AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType,
+ AggregateContext context) {
+ return doAggregate(aggregation, collectionName, outputType, context.getAggregationOperationContext());
}
@SuppressWarnings("ConstantConditions")
@@ -2185,11 +2189,10 @@ protected CloseableIterator aggregateStream(Aggregation aggregation, Stri
Assert.notNull(outputType, "Output type must not be null!");
Assert.isTrue(!aggregation.getOptions().isExplain(), "Can't use explain option with streaming!");
- AggregationUtil aggregationUtil = new AggregationUtil(queryMapper, mappingContext);
- AggregationOperationContext rootContext = aggregationUtil.prepareAggregationContext(aggregation, context);
+ AggregateContext aggregateContext = queryOperations.createAggregationContext(aggregation, context);
AggregationOptions options = aggregation.getOptions();
- List pipeline = aggregationUtil.createPipeline(aggregation, rootContext);
+ List pipeline = aggregateContext.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 67ac4fede6..ec120047e2 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
@@ -34,8 +34,12 @@
import org.springframework.data.mongodb.core.MappedDocument.MappedUpdate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
+import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
+import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
+import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
+import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
@@ -48,6 +52,7 @@
import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
@@ -194,6 +199,31 @@ DeleteContext deleteSingleContext(Query query) {
return new DeleteContext(query, false);
}
+ /**
+ * Create a new {@link AggregateContext} 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}.
+ * @since 3.2
+ */
+ AggregateContext createAggregationContext(Aggregation aggregation, @Nullable Class> inputType) {
+ return new AggregateContext(aggregation, inputType);
+ }
+
+ /**
+ * Create a new {@link AggregateContext} 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}.
+ * @since 3.2
+ */
+ AggregateContext createAggregationContext(Aggregation aggregation,
+ @Nullable AggregationOperationContext aggregationOperationContext) {
+ return new AggregateContext(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}.
@@ -341,7 +371,8 @@ private DistinctQueryContext(@Nullable Object query, String fieldName) {
}
@Override
- Document getMappedFields(@Nullable MongoPersistentEntity> entity, Class> targetType, ProjectionFactory projectionFactory) {
+ Document getMappedFields(@Nullable MongoPersistentEntity> entity, Class> targetType,
+ ProjectionFactory projectionFactory) {
return getMappedFields(entity);
}
@@ -709,7 +740,8 @@ List getUpdatePipeline(@Nullable Class> domainType) {
Class> type = domainType != null ? domainType : Object.class;
- AggregationOperationContext context = new RelaxedTypeBasedAggregationOperationContext(type, mappingContext, queryMapper);
+ AggregationOperationContext context = new RelaxedTypeBasedAggregationOperationContext(type, mappingContext,
+ queryMapper);
return aggregationUtil.createPipeline((AggregationUpdate) update, context);
}
@@ -759,4 +791,105 @@ boolean isMulti() {
return multi;
}
}
+
+ /**
+ * A context class that encapsulates common tasks required when running {@literal aggregations}.
+ *
+ * @since 3.2
+ */
+ class AggregateContext {
+
+ private Aggregation aggregation;
+ private Lazy aggregationOperationContext;
+ private Lazy> pipeline;
+ private @Nullable Class> inputType;
+
+ /**
+ * Creates new instance of {@link AggregateContext} 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
+ * the desired {@link AggregationOptions#getDomainTypeMapping() domain type mapping}.
+ * Pipelines are mapped on first access of {@link #getAggregationPipeline()} and cached for reuse.
+ *
+ * @param aggregation the source aggregation.
+ * @param aggregationOperationContext can be {@literal null}.
+ */
+ AggregateContext(Aggregation aggregation, @Nullable AggregationOperationContext aggregationOperationContext) {
+
+ this.aggregation = aggregation;
+ if (aggregation instanceof TypedAggregation) {
+ this.inputType = ((TypedAggregation) aggregation).getInputType();
+ } else if (aggregationOperationContext instanceof TypeBasedAggregationOperationContext) {
+ this.inputType = ((TypeBasedAggregationOperationContext) aggregationOperationContext).getType();
+ }
+ 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
+ * {@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
+ * {@link AggregationOptions#getDomainTypeMapping() domain type mapping}.
+ * Pipelines are mapped on first access of {@link #getAggregationPipeline()} and cached for reuse.
+ *
+ * @param aggregation the source aggregation.
+ * @param inputType can be {@literal null}.
+ */
+ AggregateContext(Aggregation aggregation, @Nullable Class> inputType) {
+
+ this.aggregation = aggregation;
+
+ if (aggregation instanceof TypedAggregation) {
+ this.inputType = ((TypedAggregation) aggregation).getInputType();
+ } else {
+ this.inputType = inputType;
+ }
+
+ this.aggregationOperationContext = Lazy
+ .of(() -> aggregationUtil.createAggregationContext(aggregation, getInputType()));
+ this.pipeline = Lazy.of(() -> aggregationUtil.createPipeline(this.aggregation, getAggregationOperationContext()));
+ }
+
+ /**
+ * Obtain the already mapped pipeline.
+ *
+ * @return never {@literal null}.
+ */
+ List getAggregationPipeline() {
+ return pipeline.get();
+ }
+
+ /**
+ * @return {@literal true} if the last aggregation stage is either {@literal $out} or {@literal $merge}.
+ * @see AggregationPipeline#isOutOrMerge()
+ */
+ boolean isOutOrMerge() {
+ return aggregation.getPipeline().isOutOrMerge();
+ }
+
+ /**
+ * Obtain the {@link AggregationOperationContext} used for mapping the pipeline.
+ *
+ * @return never {@literal null}.
+ */
+ AggregationOperationContext getAggregationOperationContext() {
+ return aggregationOperationContext.get();
+ }
+
+ /**
+ * @return the input type to map the pipeline against. Can be {@literal null}.
+ */
+ @Nullable
+ 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 76afa855a1..62673dbbfc 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,6 +17,7 @@
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
+import org.springframework.data.mongodb.core.QueryOperations.AggregateContext;
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -946,9 +947,7 @@ public Flux aggregate(TypedAggregation> aggregation, String inputCollec
Assert.notNull(aggregation, "Aggregation pipeline must not be null!");
- AggregationOperationContext context = new TypeBasedAggregationOperationContext(aggregation.getInputType(),
- mappingContext, queryMapper);
- return aggregate(aggregation, inputCollectionName, outputType, context);
+ return doAggregate(aggregation, inputCollectionName, aggregation.getInputType(), outputType);
}
/*
@@ -966,9 +965,7 @@ public Flux aggregate(TypedAggregation> aggregation, Class outputTyp
*/
@Override
public Flux aggregate(Aggregation aggregation, Class> inputType, Class outputType) {
-
- return aggregate(aggregation, getCollectionName(inputType), outputType,
- new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper));
+ return doAggregate(aggregation, getCollectionName(inputType), inputType, outputType);
}
/*
@@ -977,40 +974,29 @@ public Flux aggregate(Aggregation aggregation, Class> inputType, Class<
*/
@Override
public Flux aggregate(Aggregation aggregation, String collectionName, Class outputType) {
- return aggregate(aggregation, collectionName, outputType, null);
+ return doAggregate(aggregation, collectionName, null, outputType);
}
- /**
- * @param aggregation must not be {@literal null}.
- * @param collectionName must not be {@literal null}.
- * @param outputType must not be {@literal null}.
- * @param context can be {@literal null} and will be defaulted to {@link Aggregation#DEFAULT_CONTEXT}.
- * @return never {@literal null}.
- */
- protected Flux aggregate(Aggregation aggregation, String collectionName, Class outputType,
- @Nullable AggregationOperationContext context) {
+ protected Flux doAggregate(Aggregation aggregation, String collectionName, @Nullable Class> inputType, Class outputType) {
Assert.notNull(aggregation, "Aggregation pipeline must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
Assert.notNull(outputType, "Output type must not be null!");
- AggregationUtil aggregationUtil = new AggregationUtil(queryMapper, mappingContext);
- AggregationOperationContext rootContext = aggregationUtil.prepareAggregationContext(aggregation, context);
-
AggregationOptions options = aggregation.getOptions();
- List pipeline = aggregationUtil.createPipeline(aggregation, rootContext);
-
Assert.isTrue(!options.isExplain(), "Cannot use explain option with streaming!");
+ AggregateContext ctx = queryOperations.createAggregationContext(aggregation, inputType);
+
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Streaming aggregation: {} in collection {}", serializeToJsonSafely(pipeline), collectionName);
+ LOGGER.debug("Streaming aggregation: {} in collection {}", serializeToJsonSafely(ctx.getAggregationPipeline()), collectionName);
}
ReadDocumentCallback readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName);
return execute(collectionName,
- collection -> aggregateAndMap(collection, pipeline, aggregation.getPipeline().isOutOrMerge(), options,
+ collection -> aggregateAndMap(collection, ctx.getAggregationPipeline(), ctx.isOutOrMerge(), options,
readCallback,
- aggregation instanceof TypedAggregation ? ((TypedAggregation>) aggregation).getInputType() : null));
+ ctx.getInputType()));
}
private Flux aggregateAndMap(MongoCollection collection, List pipeline,
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 c69e75e3ec..4ef7335068 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
@@ -56,6 +56,7 @@ public class AggregationOptions {
private final Optional hint;
private Duration maxTime = Duration.ZERO;
private ResultOptions resultOptions = ResultOptions.READ;
+ private DomainTypeMapping domainTypeMapping = DomainTypeMapping.RELAXED;
/**
* Creates a new {@link AggregationOptions}.
@@ -261,6 +262,14 @@ public boolean isSkipResults() {
return ResultOptions.SKIP.equals(resultOptions);
}
+ /**
+ * @return the domain type mapping strategy do apply. Never {@literal null}.
+ * @since 3.2
+ */
+ public DomainTypeMapping getDomainTypeMapping() {
+ return domainTypeMapping;
+ }
+
/**
* Returns a new potentially adjusted copy for the given {@code aggregationCommandObject} with the configuration
* applied.
@@ -358,6 +367,7 @@ public static class Builder {
private @Nullable Document hint;
private @Nullable Duration maxTime;
private @Nullable ResultOptions resultOptions;
+ private @Nullable DomainTypeMapping domainTypeMapping;
/**
* Defines whether to off-load intensive sort-operations to disk.
@@ -475,6 +485,44 @@ public Builder skipOutput() {
return this;
}
+ /**
+ * Apply a strict domain type mapping considering {@link org.springframework.data.mongodb.core.mapping.Field}
+ * annotations throwing errors for non existing, but referenced fields.
+ *
+ * @return this.
+ * @since 3.2
+ */
+ public Builder strictMapping() {
+
+ this.domainTypeMapping = DomainTypeMapping.STRICT;
+ return this;
+ }
+
+ /**
+ * Apply a relaxed domain type mapping considering {@link org.springframework.data.mongodb.core.mapping.Field}
+ * annotations using the user provided name if a referenced field does not exist.
+ *
+ * @return this.
+ * @since 3.2
+ */
+ public Builder relaxedMapping() {
+
+ this.domainTypeMapping = DomainTypeMapping.RELAXED;
+ return this;
+ }
+
+ /**
+ * Apply no domain type mapping at all taking the pipeline as is.
+ *
+ * @return this.
+ * @since 3.2
+ */
+ public Builder noMapping() {
+
+ this.domainTypeMapping = DomainTypeMapping.NONE;
+ return this;
+ }
+
/**
* Returns a new {@link AggregationOptions} instance with the given configuration.
*
@@ -489,6 +537,9 @@ public AggregationOptions build() {
if (resultOptions != null) {
options.resultOptions = resultOptions;
}
+ if (domainTypeMapping != null) {
+ options.domainTypeMapping = domainTypeMapping;
+ }
return options;
}
@@ -508,4 +559,25 @@ private enum ResultOptions {
*/
READ;
}
+
+ /**
+ * Aggregation pipeline Domain type mappings supported by the mapping layer.
+ *
+ * @since 3.2
+ */
+ public enum DomainTypeMapping {
+
+ /**
+ * Mapping throws errors for non existing, but referenced fields.
+ */
+ STRICT,
+ /**
+ * 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.
+ */
+ NONE
+ }
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RelaxedTypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RelaxedTypeBasedAggregationOperationContext.java
index fae573e822..89cb126973 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RelaxedTypeBasedAggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RelaxedTypeBasedAggregationOperationContext.java
@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.core.aggregation;
+import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference;
@@ -55,7 +56,7 @@ protected FieldReference getReferenceFor(Field field) {
try {
return super.getReferenceFor(field);
- } catch (InvalidPersistentPropertyPath e) {
+ } catch (MappingException e) {
return new DirectFieldReference(new ExposedField(field, true));
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java
index 5cb633740e..ef9bfe73bb 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java
@@ -166,4 +166,8 @@ protected FieldReference getReferenceFor(Field field) {
return new DirectFieldReference(new ExposedField(mappedField, true));
}
+
+ public Class> getType() {
+ return type;
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
index e6f68a8c9a..11d25bb19f 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
@@ -50,7 +50,6 @@
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
-
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
@@ -66,16 +65,11 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.geo.Point;
import org.springframework.data.mapping.callback.EntityCallbacks;
+import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
import org.springframework.data.mongodb.MongoDatabaseFactory;
-import org.springframework.data.mongodb.core.aggregation.Aggregation;
-import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
-import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
-import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators;
+import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.aggregation.ComparisonOperators.Gte;
-import org.springframework.data.mongodb.core.aggregation.ConditionalOperators;
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Switch.CaseOperator;
-import org.springframework.data.mongodb.core.aggregation.Fields;
-import org.springframework.data.mongodb.core.aggregation.SetOperation;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
@@ -480,6 +474,44 @@ void aggregateShouldHonorOptionsHint() {
verify(aggregateIterable).hint(hint);
}
+ @Test // GH-3542
+ void aggregateShouldUseRelaxedMappingByDefault() {
+
+ MongoTemplate template = new MongoTemplate(factory, converter) {
+
+ @Override
+ protected AggregationResults doAggregate(Aggregation aggregation, String collectionName,
+ Class outputType, AggregationOperationContext context) {
+
+ assertThat(context).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class);
+ return super.doAggregate(aggregation, collectionName, outputType, context);
+ }
+ };
+
+ template.aggregate(
+ newAggregation(Jedi.class, Aggregation.unwind("foo")).withOptions(AggregationOptions.builder().build()),
+ Jedi.class);
+ }
+
+ @Test // GH-3542
+ void aggregateShouldUseStrictMappingIfOptionsIndicate() {
+
+ MongoTemplate template = new MongoTemplate(factory, converter) {
+
+ @Override
+ protected AggregationResults doAggregate(Aggregation aggregation, String collectionName,
+ Class outputType, AggregationOperationContext context) {
+
+ assertThat(context).isInstanceOf(TypeBasedAggregationOperationContext.class);
+ return super.doAggregate(aggregation, collectionName, outputType, context);
+ }
+ };
+
+ assertThatExceptionOfType(InvalidPersistentPropertyPath.class)
+ .isThrownBy(() -> template.aggregate(newAggregation(Jedi.class, Aggregation.unwind("foo"))
+ .withOptions(AggregationOptions.builder().strictMapping().build()), Jedi.class));
+ }
+
@Test // DATAMONGO-1166, DATAMONGO-2264
void geoNearShouldHonorReadPreferenceWhenSet() {
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
new file mode 100644
index 0000000000..6a49adf265
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryOperationsUnitTests.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.core;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+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.aggregation.Aggregation;
+import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
+import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
+import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
+import org.springframework.data.mongodb.core.convert.QueryMapper;
+import org.springframework.data.mongodb.core.convert.UpdateMapper;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+
+/**
+ * @author Christoph Strobl
+ * @since 2021/01
+ */
+@ExtendWith(MockitoExtension.class)
+class QueryOperationsUnitTests {
+
+ static final AggregationOptions NO_MAPPING = AggregationOptions.builder().noMapping().build();
+ static final AggregationOptions STRICT_MAPPING = AggregationOptions.builder().strictMapping().build();
+
+ @Mock QueryMapper queryMapper;
+ @Mock UpdateMapper updateMapper;
+ @Mock EntityOperations entityOperations;
+ @Mock PropertyOperations propertyOperations;
+ @Mock MongoDatabaseFactory mongoDbFactory;
+ @Mock MongoMappingContext mappingContext;
+
+ QueryOperations queryOperations;
+
+ @BeforeEach
+ void beforeEach() {
+
+ when(queryMapper.getMappingContext()).thenReturn((MappingContext) mappingContext);
+
+ queryOperations = new QueryOperations(queryMapper, updateMapper, entityOperations, propertyOperations,
+ mongoDbFactory);
+ }
+
+ @Test // GH-3542
+ void createAggregationContextUsesRelaxedOneForUntypedAggregationsWhenNoInputTypeProvided() {
+
+ Aggregation aggregation = Aggregation.newAggregation(Aggregation.project("name"));
+ AggregateContext ctx = queryOperations.createAggregationContext(aggregation, (Class>) null);
+
+ assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class);
+ }
+
+ @Test // GH-3542
+ void createAggregationContextUsesRelaxedOneForTypedAggregationsWhenNoInputTypeProvided() {
+
+ Aggregation aggregation = Aggregation.newAggregation(Person.class, Aggregation.project("name"));
+ AggregateContext ctx = queryOperations.createAggregationContext(aggregation, (Class>) null);
+
+ assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class);
+ }
+
+ @Test // GH-3542
+ void createAggregationContextUsesRelaxedOneForUntypedAggregationsWhenInputTypeProvided() {
+
+ Aggregation aggregation = Aggregation.newAggregation(Aggregation.project("name"));
+ AggregateContext ctx = queryOperations.createAggregationContext(aggregation, Person.class);
+
+ assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class);
+ }
+
+ @Test // GH-3542
+ void createAggregationContextUsesDefaultIfNoMappingDesired() {
+
+ Aggregation aggregation = Aggregation.newAggregation(Aggregation.project("name")).withOptions(NO_MAPPING);
+ AggregateContext ctx = queryOperations.createAggregationContext(aggregation, Person.class);
+
+ assertThat(ctx.getAggregationOperationContext()).isEqualTo(Aggregation.DEFAULT_CONTEXT);
+ }
+
+ @Test // GH-3542
+ void createAggregationContextUsesStrictlyTypedContextForTypedAggregationsWhenRequested() {
+
+ Aggregation aggregation = Aggregation.newAggregation(Person.class, Aggregation.project("name"))
+ .withOptions(STRICT_MAPPING);
+ AggregateContext ctx = queryOperations.createAggregationContext(aggregation, (Class>) null);
+
+ assertThat(ctx.getAggregationOperationContext()).isInstanceOf(TypeBasedAggregationOperationContext.class);
+ }
+
+ static class Person {
+
+ }
+}
diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc
index 99e2e0ec70..7c25cebd43 100644
--- a/src/main/asciidoc/reference/mongodb.adoc
+++ b/src/main/asciidoc/reference/mongodb.adoc
@@ -2514,7 +2514,11 @@ The actual aggregate operation is run by the `aggregate` method of the `MongoTem
+
A `TypedAggregation`, just like an `Aggregation`, holds the instructions of the aggregation pipeline and a reference to the input type, that is used for mapping domain properties to actual document fields.
+
-At runtime, field references get checked against the given input type, considering potential `@Field` annotations and raising errors when referencing nonexistent properties.
+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`.
+====
+
* `AggregationOperation`
+