Browse Source

DATAMONGO-1550 - Add $replaceRoot aggregation stage.

We now support the $replaceRoot stage in aggregation pipelines. $replaceRoot can reference either a field, an aggregation expression or it can be used to compose a replacement document.

newAggregation(
	replaceRoot().withDocument()
		.andValue("value").as("field")
		.and(MULTIPLY.of(field("total"), field("discounted")))
);

newAggregation(
	replaceRoot("item")));

Original Pull Request: #422
pull/692/head
Mark Paluch 9 years ago committed by Christoph Strobl
parent
commit
ae4cfaa58c
  1. 42
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
  2. 562
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java
  3. 26
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java
  4. 15
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java
  5. 127
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperationUnitTests.java
  6. 2
      src/main/asciidoc/reference/mongodb.adoc

42
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java

@ -29,6 +29,8 @@ import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFie
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootDocumentOperationBuilder;
import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootOperationBuilder;
import org.springframework.data.mongodb.core.aggregation.Fields.*; import org.springframework.data.mongodb.core.aggregation.Fields.*;
import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.CriteriaDefinition;
@ -216,6 +218,40 @@ public class Aggregation {
return new UnwindOperation(field(field)); return new UnwindOperation(field(field));
} }
/**
* Factory method to create a new {@link ReplaceRootOperation} for the field with the given name.
*
* @param fieldName must not be {@literal null} or empty.
* @return
* @since 1.10
*/
public static ReplaceRootOperation replaceRoot(String fieldName) {
return ReplaceRootOperation.builder().withValueOf(fieldName);
}
/**
* Factory method to create a new {@link ReplaceRootOperation} for the field with the given
* {@link AggregationExpression}.
*
* @param aggregationExpression must not be {@literal null}.
* @return
* @since 1.10
*/
public static ReplaceRootOperation replaceRoot(AggregationExpression aggregationExpression) {
return ReplaceRootOperation.builder().withValueOf(aggregationExpression);
}
/**
* Factory method to create a new {@link ReplaceRootDocumentOperationBuilder} to configure a
* {@link ReplaceRootOperation}.
*
* @return the {@literal ReplaceRootDocumentOperationBuilder}.
* @since 1.10
*/
public static ReplaceRootOperationBuilder replaceRoot() {
return ReplaceRootOperation.builder();
}
/** /**
* Factory method to create a new {@link UnwindOperation} for the field with the given name and * Factory method to create a new {@link UnwindOperation} for the field with the given name and
* {@code preserveNullAndEmptyArrays}. Note that extended unwind is supported in MongoDB version 3.2+. * {@code preserveNullAndEmptyArrays}. Note that extended unwind is supported in MongoDB version 3.2+.
@ -468,11 +504,13 @@ public class Aggregation {
if (operation instanceof FieldsExposingAggregationOperation) { if (operation instanceof FieldsExposingAggregationOperation) {
FieldsExposingAggregationOperation exposedFieldsOperation = (FieldsExposingAggregationOperation) operation; FieldsExposingAggregationOperation exposedFieldsOperation = (FieldsExposingAggregationOperation) operation;
ExposedFields fields = exposedFieldsOperation.getFields();
if (operation instanceof InheritsFieldsAggregationOperation) { if (operation instanceof InheritsFieldsAggregationOperation) {
context = new InheritingExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), context); context = new InheritingExposedFieldsAggregationOperationContext(fields, context);
} else { } else {
context = new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), context); context = fields.exposesNoFields() ? DEFAULT_CONTEXT
: new ExposedFieldsAggregationOperationContext(fields, context);
} }
} }
} }

562
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java

@ -0,0 +1,562 @@
/*
* Copyright 2016 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
*
* http://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.aggregation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.bson.Document;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.expression.spel.ast.Projection;
import org.springframework.util.Assert;
/**
* Encapsulates the aggregation framework {@code $replaceRoot}-operation.
* <p>
* We recommend to use the static factory method {@link Aggregation#replaceRoot(String)} instead of creating instances
* of this class directly.
*
* @see https://docs.mongodb.com/manual/reference/operator/aggregation/replaceRoot/#pipe._S_replaceRoot
* @author Mark Paluch
* @since 1.10
*/
public class ReplaceRootOperation implements FieldsExposingAggregationOperation {
private final Replacement replacement;
/**
* Creates a new {@link ReplaceRootOperation} given the {@link as} field name.
*
* @param field must not be {@literal null} or empty.
*/
public ReplaceRootOperation(Field field) {
this.replacement = new FieldReplacement(field);
}
/**
* Creates a new {@link ReplaceRootOperation} given the {@link as} field name.
*
* @param aggregationExpression must not be {@literal null}.
*/
public ReplaceRootOperation(AggregationExpression aggregationExpression) {
Assert.notNull(aggregationExpression, "AggregationExpression must not be null!");
this.replacement = new AggregationExpressionReplacement(aggregationExpression);
}
protected ReplaceRootOperation(Replacement replacement) {
this.replacement = replacement;
}
/**
* Creates a new {@link ReplaceRootDocumentOperationBuilder}.
*
* @return a new {@link ReplaceRootDocumentOperationBuilder}.
*/
public static ReplaceRootOperationBuilder builder() {
return new ReplaceRootOperationBuilder();
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public Document toDocument(AggregationOperationContext context) {
return new Document("$replaceRoot", new Document("newRoot", replacement.toObject(context)));
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields()
*/
@Override
public ExposedFields getFields() {
return ExposedFields.from();
}
/**
* Builder for {@link ReplaceRootOperation}.
*
* @author Mark Paluch
*/
public static class ReplaceRootOperationBuilder {
/**
* Defines a root document replacement based on a {@literal fieldName} that resolves to a document.
*
* @param fieldName must not be {@literal null} or empty.
* @return the final {@link ReplaceRootOperation}.
*/
public ReplaceRootOperation withValueOf(String fieldName) {
return new ReplaceRootOperation(Fields.field(fieldName));
}
/**
* Defines a root document replacement based on a {@link AggregationExpression} that resolves to a document.
*
* @param aggregationExpression must not be {@literal null}.
* @return the final {@link ReplaceRootOperation}.
*/
public ReplaceRootOperation withValueOf(AggregationExpression aggregationExpression) {
return new ReplaceRootOperation(aggregationExpression);
}
/**
* Defines a root document replacement based on a composable document that is empty initially.
* <p>
* {@link ReplaceRootOperation} can be populated with individual entries and derive its values from other, existing
* documents.
*
* @return the {@link ReplaceRootDocumentOperation}.
*/
public ReplaceRootDocumentOperation withDocument() {
return new ReplaceRootDocumentOperation();
}
/**
* Defines a root document replacement based on a composable document given {@literal document}
* <p>
* {@link ReplaceRootOperation} can be populated with individual entries and derive its values from other, existing
* documents.
*
* @param document must not be {@literal null}.
* @return the final {@link ReplaceRootOperation}.
*/
public ReplaceRootOperation withDocument(Document document) {
Assert.notNull(document, "Document must not be null!");
return new ReplaceRootDocumentOperation().andValuesOf(document);
}
}
/**
* Encapsulates the aggregation framework {@code $replaceRoot}-operation to result in a composable replacement
* document.
* <p>
* Instances of {@link ReplaceRootDocumentOperation} yield empty upon construction and can be populated with single
* values and documents.
*
* @author Mark Paluch
*/
static class ReplaceRootDocumentOperation extends ReplaceRootOperation {
private final static ReplacementDocument EMPTY = new ReplacementDocument();
private final ReplacementDocument current;
/**
* Creates an empty {@link ReplaceRootDocumentOperation}.
*/
public ReplaceRootDocumentOperation() {
this(EMPTY);
}
private ReplaceRootDocumentOperation(ReplacementDocument replacementDocument) {
super(replacementDocument);
current = replacementDocument;
}
/**
* Creates an extended {@link ReplaceRootDocumentOperation} that combines {@link ReplacementDocument}s from the
* {@literal currentOperation} and {@literal extension} operation.
*
* @param currentOperation must not be {@literal null}.
* @param extension must not be {@literal null}.
*/
protected ReplaceRootDocumentOperation(ReplaceRootDocumentOperation currentOperation,
ReplacementDocument extension) {
this(currentOperation.current.extendWith(extension));
}
/**
* Creates a new {@link ReplaceRootDocumentOperationBuilder} to define a field for the {@link AggregationExpression}
* .
*
* @param aggregationExpression must not be {@literal null}.
* @return the {@link ReplaceRootDocumentOperationBuilder}.
*/
public ReplaceRootDocumentOperationBuilder and(AggregationExpression aggregationExpression) {
return new ReplaceRootDocumentOperationBuilder(this, aggregationExpression);
}
/**
* Creates a new {@link ReplaceRootDocumentOperationBuilder} to define a field for the {@literal value}.
*
* @param value must not be {@literal null}.
* @return the {@link ReplaceRootDocumentOperationBuilder}.
*/
public ReplaceRootDocumentOperationBuilder andValue(Object value) {
return new ReplaceRootDocumentOperationBuilder(this, value);
}
/**
* Creates a new {@link ReplaceRootDocumentOperation} that merges all existing replacement values with values from
* {@literal value}. Existing replacement values are overwritten.
*
* @param value must not be {@literal null}.
* @return the {@link ReplaceRootDocumentOperation}.
*/
public ReplaceRootDocumentOperation andValuesOf(Object value) {
return new ReplaceRootDocumentOperation(this, ReplacementDocument.valueOf(value));
}
}
/**
* Builder for {@link ReplaceRootDocumentOperation} to populate {@link ReplacementDocument}
*
* @author Mark Paluch
*/
public static class ReplaceRootDocumentOperationBuilder {
private final ReplaceRootDocumentOperation currentOperation;
private final Object value;
protected ReplaceRootDocumentOperationBuilder(ReplaceRootDocumentOperation currentOperation, Object value) {
Assert.notNull(currentOperation, "Current ReplaceRootDocumentOperation must not be null!");
Assert.notNull(value, "Value must not be null!");
this.currentOperation = currentOperation;
this.value = value;
}
public ReplaceRootDocumentOperation as(String fieldName) {
if (value instanceof AggregationExpression) {
return new ReplaceRootDocumentOperation(currentOperation,
ReplacementDocument.forExpression(fieldName, (AggregationExpression) value));
}
return new ReplaceRootDocumentOperation(currentOperation, ReplacementDocument.forSingleValue(fieldName, value));
}
}
/**
* Replacement object that results in a replacement document or an expression that results in a document.
*
* @author Mark Paluch
*/
private abstract static class Replacement {
/**
* Renders the current {@link Replacement} into a {@link Document} based on the given
* {@link AggregationOperationContext}.
*
* @param context will never be {@literal null}.
* @return a replacement document or an expression that results in a document.
*/
public abstract Object toObject(AggregationOperationContext context);
}
/**
* {@link Replacement} that uses a {@link AggregationExpression} that results in a replacement document.
*
* @author Mark Paluch
*/
private static class AggregationExpressionReplacement extends Replacement {
private final AggregationExpression aggregationExpression;
protected AggregationExpressionReplacement(AggregationExpression aggregationExpression) {
this.aggregationExpression = aggregationExpression;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.Replacement#toObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public Document toObject(AggregationOperationContext context) {
return aggregationExpression.toDocument(context);
}
}
/**
* {@link Replacement that references a {@link Field} inside the current aggregation pipeline.
*
* @author Mark Paluch
*/
private static class FieldReplacement extends Replacement {
private final Field field;
/**
* Creates {@link FieldReplacement} given {@link Field}.
*/
protected FieldReplacement(Field field) {
Assert.notNull(field, "Field must not be null!");
this.field = field;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.Replacement#toObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public Object toObject(AggregationOperationContext context) {
return context.getReference(field).toString();
}
}
/**
* Replacement document consisting of multiple {@link ReplacementContributor}s.
*
* @author Mark Paluch
*/
private static class ReplacementDocument extends Replacement {
private final Collection<ReplacementContributor> replacements;
/**
* Creates an empty {@link ReplacementDocument}.
*/
protected ReplacementDocument() {
replacements = new ArrayList<ReplacementContributor>();
}
/**
* Creates a {@link ReplacementDocument} given {@link ReplacementContributor}.
*
* @param contributor
*/
protected ReplacementDocument(ReplacementContributor contributor) {
Assert.notNull(contributor, "ReplacementContributor must not be null!");
replacements = Collections.singleton(contributor);
}
private ReplacementDocument(Collection<ReplacementContributor> replacements) {
this.replacements = replacements;
}
/**
* Creates a {@link ReplacementDocument} given a {@literal value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static ReplacementDocument valueOf(Object value) {
return new ReplacementDocument(new DocumentContributor(value));
}
/**
* Creates a {@link ReplacementDocument} given a single {@literal field} and {@link AggregationExpression}.
*
* @param aggregationExpression must not be {@literal null}.
* @return
*/
public static ReplacementDocument forExpression(String field, AggregationExpression aggregationExpression) {
return new ReplacementDocument(new ExpressionFieldContributor(Fields.field(field), aggregationExpression));
}
/**
* Creates a {@link ReplacementDocument} given a single {@literal field} and {@literal value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static ReplacementDocument forSingleValue(String field, Object value) {
return new ReplacementDocument(new ValueFieldContributor(Fields.field(field), value));
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.Replacement#toObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public Document toObject(AggregationOperationContext context) {
Document document = new Document();
for (ReplacementContributor replacement : replacements) {
document.putAll(replacement.toDocument(context));
}
return document;
}
/**
* Extend a replacement document that merges {@code this} and {@literal replacement} {@link ReplacementContributor}s
* in a new {@link ReplacementDocument}.
*
* @param extension must not be {@literal null}.
* @return the new, extended {@link ReplacementDocument}
*/
public ReplacementDocument extendWith(ReplacementDocument extension) {
Assert.notNull(extension, "ReplacementDocument must not be null");
ReplacementDocument replacementDocument = new ReplacementDocument();
List<ReplacementContributor> replacements = new ArrayList<ReplacementContributor>(
this.replacements.size() + replacementDocument.replacements.size());
replacements.addAll(this.replacements);
replacements.addAll(extension.replacements);
return new ReplacementDocument(replacements);
}
}
/**
* Partial {@link Document} contributor for document replacement.
*
* @author Mark Paluch
*/
private abstract static class ReplacementContributor {
/**
* Renders the current {@link ReplacementContributor} into a {@link Document} based on the given
* {@link AggregationOperationContext}.
*
* @param context will never be {@literal null}.
* @return
*/
public abstract Document toDocument(AggregationOperationContext context);
}
/**
* {@link ReplacementContributor} to contribute multiple fields based on the input {@literal value}.
* <p>
* The value object is mapped into a MongoDB {@link Document}.
*
* @author Mark Paluch
*/
private static class DocumentContributor extends ReplacementContributor {
private final Object value;
/**
* Creates new {@link Projection} for the given {@link Field}.
*
* @param value must not be {@literal null}.
*/
public DocumentContributor(Object value) {
Assert.notNull(value, "Value must not be null!");
this.value = value;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplacementContributor#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public Document toDocument(AggregationOperationContext context) {
Document document = new Document("$set", value);
return (Document) context.getMappedObject(document).get("$set");
}
}
/**
* Base class for {@link ReplacementContributor} implementations to contribute a single {@literal field} Typically
* used to construct a composite document that should contain the resulting key-value pair.
*
* @author Mark Paluch
*/
private abstract static class FieldContributorSupport extends ReplacementContributor {
private final ExposedField field;
/**
* Creates new {@link FieldContributorSupport} for the given {@link Field}.
*
* @param field must not be {@literal null}.
*/
public FieldContributorSupport(Field field) {
Assert.notNull(field, "Field must not be null!");
this.field = new ExposedField(field, true);
}
/**
* @return the {@link ExposedField}.
*/
public ExposedField getField() {
return field;
}
}
/**
* {@link ReplacementContributor} to contribute a single {@literal field} and {@literal value}. The {@literal value}
* is mapped to a MongoDB {@link Document} and can be a singular value, a list or subdocument.
*
* @author Mark Paluch
*/
private static class ValueFieldContributor extends FieldContributorSupport {
private final Object value;
/**
* Creates new {@link Projection} for the given {@link Field}.
*
* @param field must not be {@literal null}.
* @param value must not be {@literal null}.
*/
public ValueFieldContributor(Field field, Object value) {
super(field);
Assert.notNull(value, "Value must not be null!");
this.value = value;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplacementContributor#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public Document toDocument(AggregationOperationContext context) {
Document document = new Document("$set", value);
return new Document(getField().getTarget(), context.getMappedObject(document).get("$set"));
}
}
/**
* {@link ReplacementContributor} to contribute a single {@literal field} and value based on a
* {@link AggregationExpression}.
*
* @author Mark Paluch
*/
private static class ExpressionFieldContributor extends FieldContributorSupport {
private final AggregationExpression aggregationExpression;
/**
* Creates new {@link Projection} for the given {@link Field}.
*
* @param field must not be {@literal null}.
* @param aggregationExpression must not be {@literal null}.
*/
public ExpressionFieldContributor(Field field, AggregationExpression aggregationExpression) {
super(field);
Assert.notNull(aggregationExpression, "AggregationExpression must not be null!");
this.aggregationExpression = aggregationExpression;
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplacementContributor#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public Document toDocument(AggregationOperationContext context) {
return new Document(getField().getTarget(), aggregationExpression.toDocument(context));
}
}
}

26
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java

@ -1052,6 +1052,32 @@ public class AggregationTests {
assertThat((Integer) document.get("millisecond"), is(789)); assertThat((Integer) document.get("millisecond"), is(789));
} }
/**
* @see DATAMONGO-1550
*/
@Test
public void shouldPerformReplaceRootOperatorCorrectly() throws ParseException {
assumeTrue(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_FOUR));
Data data = new Data();
DataItem dataItem = new DataItem();
dataItem.primitiveIntValue = 42;
data.item = dataItem;
mongoTemplate.insert(data);
TypedAggregation<Data> agg = newAggregation(Data.class, project("item"), //
replaceRoot("item"), //
project().and("primitiveIntValue").as("my_primitiveIntValue"));
AggregationResults<Document> results = mongoTemplate.aggregate(agg, Document.class);
Document resultDocument = results.getUniqueMappedResult();
assertThat(resultDocument, is(notNullValue()));
assertThat((Integer) resultDocument.get("my_primitiveIntValue"), is(42));
assertThat((Integer) resultDocument.keySet().size(), is(1));
}
/** /**
* @see DATAMONGO-788 * @see DATAMONGO-788
*/ */

15
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java

@ -155,6 +155,21 @@ public class AggregationUnitTests {
isBsonObject().notContaining("includeArrayIndex").containing("preserveNullAndEmptyArrays", true)); isBsonObject().notContaining("includeArrayIndex").containing("preserveNullAndEmptyArrays", true));
} }
/**
* @see DATAMONGO-1550
*/
@Test
public void replaceRootOperationShouldBuildCorrectClause() {
Document agg = newAggregation( //
replaceRoot().withDocument().andValue("value").as("field")) //
.toDocument("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
Document unwind = ((List<Document>) agg.get("pipeline")).get(0);
assertThat(unwind, isBsonObject().containing("$replaceRoot.newRoot", new Document("field", "value")));
}
/** /**
* @see DATAMONGO-753 * @see DATAMONGO-753
*/ */

127
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperationUnitTests.java

@ -0,0 +1,127 @@
/*
* Copyright 2016 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
*
* http://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.aggregation;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.bson.Document;
import org.junit.Test;
import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.VariableOperators;
import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootDocumentOperation;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.util.JSON;
/**
* Unit tests for {@link ReplaceRootOperation}.
*
* @author Mark Paluch
*/
public class ReplaceRootOperationUnitTests {
/**
* @see DATAMONGO-1550
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullField() {
new ReplaceRootOperation((Field) null);
}
/**
* @see DATAMONGO-1550
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullExpression() {
new ReplaceRootOperation((AggregationExpression) null);
}
/**
* @see DATAMONGO-1550
*/
@Test
public void shouldRenderCorrectly() {
ReplaceRootOperation operation = ReplaceRootDocumentOperation.builder()
.withDocument(new Document("hello", "world"));
Document dbObject = operation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(dbObject, is(Document.parse("{ $replaceRoot : { newRoot: { hello: \"world\" } } }")));
}
/**
* @see DATAMONGO-1550
*/
@Test
public void shouldRenderExpressionCorrectly() {
ReplaceRootOperation operation = new ReplaceRootOperation(VariableOperators //
.mapItemsOf("array") //
.as("element") //
.andApply(AggregationFunctionExpressions.MULTIPLY.of("$$element", 10)));
Document dbObject = operation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(dbObject, is(Document.parse("{ $replaceRoot : { newRoot : { "
+ "$map : { input : \"$array\" , as : \"element\" , in : { $multiply : [ \"$$element\" , 10]} } " + "} } }")));
}
/**
* @see DATAMONGO-1550
*/
@Test
public void shouldComposeDocument() {
ReplaceRootOperation operation = ReplaceRootDocumentOperation.builder().withDocument() //
.andValue("value").as("key") //
.and(AggregationFunctionExpressions.MULTIPLY.of("$$element", 10)).as("multiply");
Document dbObject = operation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(dbObject, is(Document
.parse("{ $replaceRoot : { newRoot: { key: \"value\", multiply: { $multiply : [ \"$$element\" , 10]} } } }")));
}
/**
* @see DATAMONGO-1550
*/
@Test
public void shouldComposeSubDocument() {
Document partialReplacement = new Document("key", "override").append("key2", "value2");
ReplaceRootOperation operation = ReplaceRootDocumentOperation.builder().withDocument() //
.andValue("value").as("key") //
.andValuesOf(partialReplacement);
Document dbObject = operation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(dbObject, is(Document.parse("{ $replaceRoot : { newRoot: { key: \"override\", key2: \"value2\"} } } }")));
}
/**
* @see DATAMONGO-1550
*/
@Test
public void shouldNotExposeFields() {
ReplaceRootOperation operation = new ReplaceRootOperation(Fields.field("field"));
assertThat(operation.getFields().exposesNoFields(), is(true));
assertThat(operation.getFields().exposesSingleFieldOnly(), is(false));
}
}

2
src/main/asciidoc/reference/mongodb.adoc

@ -1676,7 +1676,7 @@ At the time of this writing we provide support for the following Aggregation Ope
[cols="2*"] [cols="2*"]
|=== |===
| Pipeline Aggregation Operators | Pipeline Aggregation Operators
| project, skip, limit, lookup, unwind, group, sort, geoNear | count, geoNear, group, limit, lookup, match, project, replaceRoot, skip, sort, unwind
| Set Aggregation Operators | Set Aggregation Operators
| setEquals, setIntersection, setUnion, setDifference, setIsSubset, anyElementTrue, allElementsTrue | setEquals, setIntersection, setUnion, setDifference, setIsSubset, anyElementTrue, allElementsTrue

Loading…
Cancel
Save