Use relaxed type mapping for aggregations by default.

This commit switches from a strict to a relaxed type mapping for aggregation executions. This allows users to add fields to the aggregation that might be part of the stored document but not necessarily of its java model representation.
Instead of throwing an exception in those cases the relaxed type check will go on with the user provided field names.
To restore the original behaviour use the strictMapping() option on AggregationOptions.

Closes #3542
Original pull request: #3545.
This commit is contained in:
Christoph Strobl
2021-01-25 11:43:08 +01:00
committed by Mark Paluch
parent 91f1dc1c6a
commit 4700b4dda2
10 changed files with 415 additions and 61 deletions

View File

@@ -27,6 +27,7 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation;
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.MongoPersistentEntity;
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<AggregationOperationContext> 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);
}
/**

View File

@@ -55,6 +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.CountContext;
import org.springframework.data.mongodb.core.QueryOperations.DeleteContext;
import org.springframework.data.mongodb.core.QueryOperations.DistinctQueryContext;
@@ -1988,7 +1989,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
public <O> AggregationResults<O> aggregate(Aggregation aggregation, Class<?> inputType, Class<O> outputType) {
return aggregate(aggregation, getCollectionName(inputType), outputType,
new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper));
queryOperations.createAggregationContext(aggregation, inputType).getAggregationOperationContext());
}
/* (non-Javadoc)
@@ -2095,9 +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!");
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 <O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName, Class<O> outputType,
AggregateContext context) {
return doAggregate(aggregation, collectionName, outputType, context.getAggregationOperationContext());
}
@SuppressWarnings("ConstantConditions")
@@ -2185,11 +2189,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!");
AggregationUtil aggregationUtil = new AggregationUtil(queryMapper, mappingContext);
AggregationOperationContext rootContext = aggregationUtil.prepareAggregationContext(aggregation, context);
AggregateContext aggregateContext = queryOperations.createAggregationContext(aggregation, context);
AggregationOptions options = aggregation.getOptions();
List<Document> pipeline = aggregationUtil.createPipeline(aggregation, rootContext);
List<Document> pipeline = aggregateContext.getAggregationPipeline();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Streaming aggregation: {} in collection {}", serializeToJsonSafely(pipeline), collectionName);

View File

@@ -34,8 +34,12 @@ import org.springframework.data.mongodb.CodecRegistryProvider;
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;
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 @@ class QueryOperations {
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 @@ class QueryOperations {
}
@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 @@ class QueryOperations {
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 @@ class QueryOperations {
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> aggregationOperationContext;
private Lazy<List<Document>> 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. <br />
* Creates a new {@link AggregationOperationContext} if none given, based on the {@link Aggregation} input type and
* the desired {@link AggregationOptions#getDomainTypeMapping() domain type mapping}. <br />
* 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. <br />
* Creates a new {@link AggregationOperationContext} based on the {@link Aggregation} input type and the desired
* {@link AggregationOptions#getDomainTypeMapping() domain type mapping}. <br />
* 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<Document> 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());
}
}
}

View File

@@ -17,6 +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.aggregation.RelaxedTypeBasedAggregationOperationContext;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -946,9 +947,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
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 class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
*/
@Override
public <O> Flux<O> aggregate(Aggregation aggregation, Class<?> inputType, Class<O> outputType) {
return aggregate(aggregation, getCollectionName(inputType), outputType,
new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper));
return doAggregate(aggregation, getCollectionName(inputType), inputType, outputType);
}
/*
@@ -977,40 +974,29 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
*/
@Override
public <O> Flux<O> aggregate(Aggregation aggregation, String collectionName, Class<O> 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 <O> Flux<O> aggregate(Aggregation aggregation, String collectionName, Class<O> outputType,
@Nullable AggregationOperationContext context) {
protected <O> Flux<O> doAggregate(Aggregation aggregation, String collectionName, @Nullable Class<?> inputType, Class<O> 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<Document> 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<O> 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 <O> Flux<O> aggregateAndMap(MongoCollection<Document> collection, List<Document> pipeline,

View File

@@ -56,6 +56,7 @@ public class AggregationOptions {
private final Optional<Document> 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 class AggregationOptions {
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 class AggregationOptions {
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 class AggregationOptions {
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 class AggregationOptions {
if (resultOptions != null) {
options.resultOptions = resultOptions;
}
if (domainTypeMapping != null) {
options.domainTypeMapping = domainTypeMapping;
}
return options;
}
@@ -508,4 +559,25 @@ public class AggregationOptions {
*/
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
}
}

View File

@@ -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 @@ public class RelaxedTypeBasedAggregationOperationContext extends TypeBasedAggreg
try {
return super.getReferenceFor(field);
} catch (InvalidPersistentPropertyPath e) {
} catch (MappingException e) {
return new DirectFieldReference(new ExposedField(field, true));
}
}

View File

@@ -166,4 +166,8 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio
return new DirectFieldReference(new ExposedField(mappedField, true));
}
public Class<?> getType() {
return type;
}
}

View File

@@ -50,7 +50,6 @@ import org.mockito.Mock;
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.convert.CustomConversions;
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 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
verify(aggregateIterable).hint(hint);
}
@Test // GH-3542
void aggregateShouldUseRelaxedMappingByDefault() {
MongoTemplate template = new MongoTemplate(factory, converter) {
@Override
protected <O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName,
Class<O> 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 <O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName,
Class<O> 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() {

View File

@@ -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 {
}
}

View File

@@ -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`
+