DATAMONGO-2363 - Add support for $merge and $addFields aggregation stage.
MergeOperation.builder().intoCollection("monthlytotals")
.whenDocumentsMatchApply(
newAggregation(
AddFieldsOperation.builder()
.addField("thumbsup").withValueOf(sumOf("thumbsup").and("$$new.thumbsup"))
.addField("thumbsdown").withValueOf(sumOf("thumbsdown").and("$$new.thumbsdown"))
.build()
)
)
.whenDocumentsDontMatch(insertNewDocument())
.build()
Original pull request: #801.
This commit is contained in:
committed by
Mark Paluch
parent
7e3f7bd861
commit
a4e12a96c9
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2020 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.aggregation;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation.AddFieldsOperationBuilder.ValueAppender;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Adds new fields to documents. {@code $addFields} outputs documents that contain all existing fields from the input
|
||||
* documents and newly added fields.
|
||||
*
|
||||
* <pre class="code">
|
||||
* AddFieldsOperation.addField("totalHomework").withValue("A+").and().addField("totalQuiz").withValue("B-")
|
||||
* </pre>
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 3.0
|
||||
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/addFields/">MongoDB Aggregation
|
||||
* Framework: $addFields</a>
|
||||
*/
|
||||
public class AddFieldsOperation extends DocumentEnhancingOperation {
|
||||
|
||||
/**
|
||||
* Create new instance of {@link AddFieldsOperation} adding map keys as exposed fields.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
*/
|
||||
private AddFieldsOperation(Map<Object, Object> source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new instance of {@link AddFieldsOperation}
|
||||
*
|
||||
* @param field must not be {@literal null}.
|
||||
* @param value can be {@literal null}.
|
||||
*/
|
||||
public AddFieldsOperation(Object field, @Nullable Object value) {
|
||||
this(Collections.singletonMap(field, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the {@link AddFieldsOperation} via {@link AddFieldsOperationBuilder}.
|
||||
*
|
||||
* @return new instance of {@link AddFieldsOperationBuilder}.
|
||||
*/
|
||||
public static AddFieldsOperationBuilder builder() {
|
||||
return new AddFieldsOperationBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate another field to add.
|
||||
*
|
||||
* @param field must not be {@literal null}.
|
||||
* @return new instance of {@link AddFieldsOperationBuilder}.
|
||||
*/
|
||||
public static ValueAppender addField(String field) {
|
||||
return new AddFieldsOperationBuilder().addField(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the value for a specific field to the operation.
|
||||
*
|
||||
* @param field the target field to add.
|
||||
* @param value the value to assign.
|
||||
* @return new instance of {@link AddFieldsOperation}.
|
||||
*/
|
||||
public AddFieldsOperation addField(Object field, Object value) {
|
||||
|
||||
LinkedHashMap<Object, Object> target = new LinkedHashMap<>(getValueMap());
|
||||
target.put(field, value);
|
||||
|
||||
return new AddFieldsOperation(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate additional fields to add.
|
||||
*
|
||||
* @return new instance of {@link AddFieldsOperationBuilder}.
|
||||
*/
|
||||
public AddFieldsOperationBuilder and() {
|
||||
return new AddFieldsOperationBuilder(getValueMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String mongoOperator() {
|
||||
return "$addFields";
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 3.0
|
||||
*/
|
||||
public static class AddFieldsOperationBuilder {
|
||||
|
||||
private final Map<Object, Object> valueMap;
|
||||
|
||||
private AddFieldsOperationBuilder() {
|
||||
this.valueMap = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private AddFieldsOperationBuilder(Map<Object, Object> source) {
|
||||
this.valueMap = new LinkedHashMap<>(source);
|
||||
}
|
||||
|
||||
public AddFieldsOperationBuilder addFieldWithValue(String field, @Nullable Object value) {
|
||||
return addField(field).withValue(value);
|
||||
}
|
||||
|
||||
public AddFieldsOperationBuilder addFieldWithValueOf(String field, Object value) {
|
||||
return addField(field).withValueOf(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the field to add.
|
||||
*
|
||||
* @param field must not be {@literal null}.
|
||||
* @return new instance of {@link ValueAppender}.
|
||||
*/
|
||||
public ValueAppender addField(String field) {
|
||||
|
||||
return new ValueAppender() {
|
||||
|
||||
@Override
|
||||
public AddFieldsOperationBuilder withValue(Object value) {
|
||||
|
||||
valueMap.put(field, value);
|
||||
return AddFieldsOperationBuilder.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddFieldsOperationBuilder withValueOf(Object value) {
|
||||
|
||||
valueMap.put(field, value instanceof String ? Fields.fields((String) value) : value);
|
||||
return AddFieldsOperationBuilder.this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public AddFieldsOperation build() {
|
||||
return new AddFieldsOperation(valueMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 3.0
|
||||
*/
|
||||
public interface ValueAppender {
|
||||
|
||||
/**
|
||||
* Define the value to assign as is.
|
||||
*
|
||||
* @param value can be {@literal null}.
|
||||
* @return new instance of {@link AddFieldsOperation}.
|
||||
*/
|
||||
AddFieldsOperationBuilder withValue(@Nullable Object value);
|
||||
|
||||
/**
|
||||
* Define the value to assign. Plain {@link String} values are treated as {@link Field field references}.
|
||||
*
|
||||
* @param value must not be {@literal null}.
|
||||
* @return new instance of {@link AddFieldsOperation}.
|
||||
*/
|
||||
AddFieldsOperationBuilder withValueOf(Object value);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,11 @@ import java.util.List;
|
||||
import org.bson.Document;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation.AddFieldsOperationBuilder;
|
||||
import org.springframework.data.mongodb.core.aggregation.CountOperation.CountOperationBuilder;
|
||||
import org.springframework.data.mongodb.core.aggregation.FacetOperation.FacetOperationBuilder;
|
||||
import org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.StartWithBuilder;
|
||||
import org.springframework.data.mongodb.core.aggregation.MergeOperation.MergeOperationBuilder;
|
||||
import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootDocumentOperationBuilder;
|
||||
import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootOperationBuilder;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
@@ -647,6 +649,28 @@ public class Aggregation {
|
||||
return new GeoNearOperation(query, distanceField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a {@link MergeOperationBuilder builder} instance to create a new {@link MergeOperation}.
|
||||
*
|
||||
* @return new instance of {@link MergeOperationBuilder}.
|
||||
* @see MergeOperation
|
||||
* @since 3.0
|
||||
*/
|
||||
public static MergeOperationBuilder merge() {
|
||||
return MergeOperation.builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain an {@link AddFieldsOperationBuilder builder} instance to create a new {@link AddFieldsOperation}.
|
||||
*
|
||||
* @return new instance of {@link AddFieldsOperationBuilder}.
|
||||
* @see AddFieldsOperation
|
||||
* @since 3.0
|
||||
*/
|
||||
public static AddFieldsOperationBuilder addFields() {
|
||||
return AddFieldsOperation.builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link AggregationOptions.Builder}.
|
||||
*
|
||||
|
||||
@@ -20,7 +20,6 @@ import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bson.Document;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -103,4 +102,16 @@ public interface AggregationOperationContext {
|
||||
.map(PropertyDescriptor::getName) //
|
||||
.toArray(String[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* This toggle allows the {@link AggregationOperationContext context} to use any given field name without checking for
|
||||
* its existence. Typically the {@link AggregationOperationContext} fails when referencing unknown fields, those that
|
||||
* are not present in one of the previous stages or the input source, throughout the pipeline.
|
||||
*
|
||||
* @return a more relaxed {@link AggregationOperationContext}.
|
||||
* @since 3.0
|
||||
*/
|
||||
default AggregationOperationContext continueOnMissingFieldReference() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2020 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.aggregation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
|
||||
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
|
||||
|
||||
/**
|
||||
* Base class for common taks required by {@link SetOperation} and {@link AddFieldsOperation}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 3.0
|
||||
*/
|
||||
abstract class DocumentEnhancingOperation implements InheritsFieldsAggregationOperation {
|
||||
|
||||
private Map<Object, Object> valueMap;
|
||||
private ExposedFields exposedFields = ExposedFields.empty();
|
||||
|
||||
protected DocumentEnhancingOperation(Map<Object, Object> source) {
|
||||
|
||||
this.valueMap = new LinkedHashMap<>(source);
|
||||
for (Object key : source.keySet()) {
|
||||
this.exposedFields = add(key);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
|
||||
*/
|
||||
@Override
|
||||
public Document toDocument(AggregationOperationContext context) {
|
||||
|
||||
InheritingExposedFieldsAggregationOperationContext operationContext = new InheritingExposedFieldsAggregationOperationContext(
|
||||
exposedFields, context);
|
||||
|
||||
if (valueMap.size() == 1) {
|
||||
return context.getMappedObject(
|
||||
new Document(mongoOperator(), toSetEntry(valueMap.entrySet().iterator().next(), operationContext)));
|
||||
}
|
||||
|
||||
Document $set = new Document();
|
||||
valueMap.entrySet().stream().map(it -> toSetEntry(it, operationContext)).forEach($set::putAll);
|
||||
return context.getMappedObject(new Document(mongoOperator(), $set));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the String representation of the native MongoDB operator.
|
||||
*/
|
||||
protected abstract String mongoOperator();
|
||||
|
||||
/**
|
||||
* @return the raw value map
|
||||
*/
|
||||
protected Map<Object, Object> getValueMap() {
|
||||
return this.valueMap;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields()
|
||||
*/
|
||||
@Override
|
||||
public ExposedFields getFields() {
|
||||
return exposedFields;
|
||||
}
|
||||
|
||||
private ExposedFields add(Object field) {
|
||||
|
||||
if (field instanceof Field) {
|
||||
return exposedFields.and(new ExposedField((Field) field, true));
|
||||
}
|
||||
if (field instanceof String) {
|
||||
return exposedFields.and(new ExposedField(Fields.field((String) field), true));
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(String.format("Expected %s to be a field/property.", field));
|
||||
}
|
||||
|
||||
private static Document toSetEntry(Entry<Object, Object> entry, AggregationOperationContext context) {
|
||||
|
||||
String field = entry.getKey() instanceof String ? context.getReference((String) entry.getKey()).getRaw()
|
||||
: context.getReference((Field) entry.getKey()).getRaw();
|
||||
|
||||
Object value = computeValue(entry.getValue(), context);
|
||||
|
||||
return new Document(field, value);
|
||||
}
|
||||
|
||||
private static Object computeValue(Object value, AggregationOperationContext context) {
|
||||
|
||||
if (value instanceof Field) {
|
||||
return context.getReference((Field) value).toString();
|
||||
}
|
||||
if (value instanceof AggregationExpression) {
|
||||
return ((AggregationExpression) value).toDocument(context);
|
||||
}
|
||||
if (value instanceof Collection) {
|
||||
return ((Collection) value).stream().map(it -> computeValue(it, context)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
/*
|
||||
* Copyright 2020 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.aggregation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.bson.Document;
|
||||
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.VariableOperators.Let;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Encapsulates the {@code $merge}-operation.
|
||||
* <p>
|
||||
* We recommend to use the {@link MergeOperationBuilder builder} via {@link MergeOperation#builder()} instead of creating
|
||||
* instances of this class directly.
|
||||
*
|
||||
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/merge/">MongoDB Documentation</a>
|
||||
* @author Christoph Strobl
|
||||
* @since 3.0
|
||||
*/
|
||||
public class MergeOperation implements FieldsExposingAggregationOperation, InheritsFieldsAggregationOperation {
|
||||
|
||||
private MergeOperationTarget into;
|
||||
private UniqueMergeId on;
|
||||
private @Nullable Let let;
|
||||
private @Nullable WhenDocumentsMatch whenMatched;
|
||||
private @Nullable WhenDocumentsDontMatch whenNotMatched;
|
||||
|
||||
/**
|
||||
* Create new instance of {@link MergeOperation}.
|
||||
*
|
||||
* @param into the target (collection and database)
|
||||
* @param on the unique identifier. Can be {@literal null}.
|
||||
* @param let exposed variables for {@link WhenDocumentsMatch#updateWith(Aggregation)}. Can be {@literal null}.
|
||||
* @param whenMatched behavior if a result document matches an existing one in the target collection. Can be
|
||||
* {@literal null}.
|
||||
* @param whenNotMatched behavior if a result document does not match an existing one in the target collection. Can be
|
||||
* {@literal null}.
|
||||
*/
|
||||
public MergeOperation(MergeOperationTarget into, UniqueMergeId on, @Nullable Let let,
|
||||
@Nullable WhenDocumentsMatch whenMatched, @Nullable WhenDocumentsDontMatch whenNotMatched) {
|
||||
|
||||
Assert.notNull(into, "Into must not be null! Please provide a target collection.");
|
||||
Assert.notNull(on, "On must not be null! Use Collections.emptySet() instead.");
|
||||
|
||||
this.into = into;
|
||||
this.on = on;
|
||||
this.let = let;
|
||||
this.whenMatched = whenMatched;
|
||||
this.whenNotMatched = whenNotMatched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified form to apply all default options for {@code $merge} (including writing to a collection in the same
|
||||
* database).
|
||||
*
|
||||
* @param collection the output collection within the same database.
|
||||
* @return new instance of {@link MergeOperation}.
|
||||
*/
|
||||
public static MergeOperation mergeInto(String collection) {
|
||||
return builder().intoCollection(collection).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the {@link MergeOperationBuilder builder API} to create a new instance of {@link MergeOperation}.
|
||||
*
|
||||
* @return new instance of {@link MergeOperationBuilder}.
|
||||
*/
|
||||
public static MergeOperationBuilder builder() {
|
||||
return new MergeOperationBuilder();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.aggregation.Aggregation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
|
||||
*/
|
||||
@Override
|
||||
public Document toDocument(AggregationOperationContext context) {
|
||||
|
||||
if (isJustCollection()) {
|
||||
return new Document("$merge", into.collection);
|
||||
}
|
||||
|
||||
Document $merge = new Document();
|
||||
$merge.putAll(into.toDocument(context));
|
||||
|
||||
if (!on.isJustIdField()) {
|
||||
$merge.putAll(on.toDocument(context));
|
||||
}
|
||||
if (let != null) {
|
||||
$merge.append("let", let.toDocument(context).get("$let", Document.class).get("vars"));
|
||||
}
|
||||
if (whenMatched != null) {
|
||||
$merge.putAll(whenMatched.toDocument(context));
|
||||
}
|
||||
if (whenNotMatched != null) {
|
||||
$merge.putAll(whenNotMatched.toDocument(context));
|
||||
|
||||
}
|
||||
|
||||
return new Document("$merge", $merge);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields()
|
||||
*/
|
||||
@Override
|
||||
public ExposedFields getFields() {
|
||||
|
||||
if (let == null) {
|
||||
return ExposedFields.from();
|
||||
}
|
||||
|
||||
return ExposedFields.synthetic(Fields.fields(let.getVariableNames()));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation#inheritsFields()
|
||||
*/
|
||||
@Override
|
||||
public boolean inheritsFields() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if nothing more than the collection is specified.
|
||||
*/
|
||||
private boolean isJustCollection() {
|
||||
return into.isTargetingSameDatabase() && on.isJustIdField() && let == null && whenMatched == null
|
||||
&& whenNotMatched == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value object representing the unique id used during the merge operation to identify duplicates in the target
|
||||
* collection.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.3
|
||||
*/
|
||||
public static class UniqueMergeId {
|
||||
|
||||
private static final UniqueMergeId ID = new UniqueMergeId(Collections.emptyList());
|
||||
|
||||
private Collection<String> uniqueIdentifier;
|
||||
|
||||
private UniqueMergeId(Collection<String> uniqueIdentifier) {
|
||||
this.uniqueIdentifier = uniqueIdentifier;
|
||||
}
|
||||
|
||||
public static UniqueMergeId ofIdFields(String... fields) {
|
||||
|
||||
if (ObjectUtils.isEmpty(fields)) {
|
||||
return id();
|
||||
}
|
||||
|
||||
return new UniqueMergeId(Arrays.asList(fields));
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge Documents by using the MongoDB {@literal _id} field.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static UniqueMergeId id() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
boolean isJustIdField() {
|
||||
return this.equals(ID);
|
||||
}
|
||||
|
||||
Document toDocument(AggregationOperationContext context) {
|
||||
|
||||
List<String> mappedOn = uniqueIdentifier.stream().map(context::getReference).map(FieldReference::getRaw)
|
||||
.collect(Collectors.toList());
|
||||
return new Document("on", mappedOn.size() == 1 ? mappedOn.iterator().next() : mappedOn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Value Object representing the {@code into} field of a {@code $merge} aggregation stage. <br />
|
||||
* If not stated explicitly via {@link MergeOperationTarget#inDatabase(String)} the {@literal collection} is created
|
||||
* in the very same {@literal database}. In this case {@code into} is just a single String holding the collection
|
||||
* name. <br />
|
||||
*
|
||||
* <pre class="code">
|
||||
* into: "target-collection-name"
|
||||
* </pre>
|
||||
*
|
||||
* If the collection needs to be in a different database {@code into} will be a {@link Document} like the following
|
||||
*
|
||||
* <pre class="code">
|
||||
* {
|
||||
* into: {}
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.3
|
||||
*/
|
||||
public static class MergeOperationTarget {
|
||||
|
||||
private final @Nullable String database;
|
||||
private final String collection;
|
||||
|
||||
private MergeOperationTarget(@Nullable String database, String collection) {
|
||||
|
||||
Assert.hasText(collection, "Collection must not be null nor empty!");
|
||||
|
||||
this.database = database;
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param collection The output collection results will be stored in. Must not be {@literal null}.
|
||||
* @return new instance of {@link MergeOperationTarget}.
|
||||
*/
|
||||
public static MergeOperationTarget collection(String collection) {
|
||||
return new MergeOperationTarget(null, collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally specify the target database if different from the source one.
|
||||
*
|
||||
* @param database must not be {@literal null}.
|
||||
* @return new instance of {@link MergeOperationTarget}.
|
||||
*/
|
||||
public MergeOperationTarget inDatabase(String database) {
|
||||
return new MergeOperationTarget(database, collection);
|
||||
}
|
||||
|
||||
boolean isTargetingSameDatabase() {
|
||||
return !StringUtils.hasText(database);
|
||||
}
|
||||
|
||||
Document toDocument(AggregationOperationContext context) {
|
||||
|
||||
return new Document("into",
|
||||
!StringUtils.hasText(database) ? collection : new Document("db", database).append("coll", collection));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Value Object specifying how to deal with a result document that matches an existing document in the collection
|
||||
* based on the fields of the {@code on} property describing the unique identifier.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.3
|
||||
*/
|
||||
public static class WhenDocumentsMatch {
|
||||
|
||||
private final Object value;
|
||||
|
||||
private WhenDocumentsMatch(Object value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static WhenDocumentsMatch whenMatchedOf(String value) {
|
||||
return new WhenDocumentsMatch(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the existing document in the output collection with the matching results document.
|
||||
*
|
||||
* @return new instance of {@link WhenDocumentsMatch}.
|
||||
*/
|
||||
public static WhenDocumentsMatch replaceDocument() {
|
||||
return whenMatchedOf("replace");
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the existing document in the output collection.
|
||||
*
|
||||
* @return new instance of {@link WhenDocumentsMatch}.
|
||||
*/
|
||||
public static WhenDocumentsMatch keepExistingDocument() {
|
||||
return whenMatchedOf("keepExisting");
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the matching documents. Please see the MongoDB reference documentation for details.
|
||||
*
|
||||
* @return new instance of {@link WhenDocumentsMatch}.
|
||||
*/
|
||||
public static WhenDocumentsMatch mergeDocuments() {
|
||||
return whenMatchedOf("merge");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop and fail the aggregation operation. Does not revert already performed changes on previous documents.
|
||||
*
|
||||
* @return new instance of {@link WhenDocumentsMatch}.
|
||||
*/
|
||||
public static WhenDocumentsMatch failOnMatch() {
|
||||
return whenMatchedOf("fail");
|
||||
}
|
||||
|
||||
/**
|
||||
* Use an {@link Aggregation} to update the document in the collection. Please see the MongoDB reference
|
||||
* documentation for details.
|
||||
*
|
||||
* @param aggregation must not be {@literal null}.
|
||||
* @return new instance of {@link WhenDocumentsMatch}.
|
||||
*/
|
||||
public static WhenDocumentsMatch updateWith(Aggregation aggregation) {
|
||||
return new WhenDocumentsMatch(aggregation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use an aggregation pipeline to update the document in the collection. Please see the MongoDB reference
|
||||
* documentation for details.
|
||||
*
|
||||
* @param aggregationPipeline must not be {@literal null}.
|
||||
* @return new instance of {@link WhenDocumentsMatch}.
|
||||
*/
|
||||
public static WhenDocumentsMatch updateWith(List<AggregationOperation> aggregationPipeline) {
|
||||
return new WhenDocumentsMatch(aggregationPipeline);
|
||||
}
|
||||
|
||||
Document toDocument(AggregationOperationContext context) {
|
||||
|
||||
if (value instanceof Aggregation) {
|
||||
return new Document("whenMatched", ((Aggregation) value).toPipeline(context));
|
||||
}
|
||||
|
||||
return new Document("whenMatched", value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Value Object specifying how to deal with a result document that do not match an existing document in the collection
|
||||
* based on the fields of the {@code on} property describing the unique identifier.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.3
|
||||
*/
|
||||
public static class WhenDocumentsDontMatch {
|
||||
|
||||
private final String value;
|
||||
|
||||
private WhenDocumentsDontMatch(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static WhenDocumentsDontMatch whenNotMatchedOf(String value) {
|
||||
return new WhenDocumentsDontMatch(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the document into the output collection.
|
||||
*
|
||||
* @return new instance of {@link WhenDocumentsDontMatch}.
|
||||
*/
|
||||
public static WhenDocumentsDontMatch insertNewDocument() {
|
||||
return whenNotMatchedOf("insert");
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard the document - do not insert the document into the output collection.
|
||||
*
|
||||
* @return new instance of {@link WhenDocumentsDontMatch}.
|
||||
*/
|
||||
public static WhenDocumentsDontMatch discardDocument() {
|
||||
return whenNotMatchedOf("discard");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop and fail the aggregation operation. Does not revert already performed changes on previous documents.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static WhenDocumentsDontMatch failWhenNotMatch() {
|
||||
return whenNotMatchedOf("fail");
|
||||
}
|
||||
|
||||
public Document toDocument(AggregationOperationContext context) {
|
||||
return new Document("whenNotMatched", value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder API to construct a {@link MergeOperation}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.3
|
||||
*/
|
||||
public static class MergeOperationBuilder {
|
||||
|
||||
private String collection;
|
||||
private @Nullable String database;
|
||||
private @Nullable UniqueMergeId id;
|
||||
private @Nullable Let let;
|
||||
private @Nullable WhenDocumentsMatch whenMatched;
|
||||
private @Nullable WhenDocumentsDontMatch whenNotMatched;
|
||||
|
||||
public MergeOperationBuilder() {}
|
||||
|
||||
/**
|
||||
* Required output collection name to store results to.
|
||||
*
|
||||
* @param collection must not be {@literal null} nor empty.
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder intoCollection(String collection) {
|
||||
|
||||
Assert.hasText(collection, "Collection must not be null nor empty!");
|
||||
|
||||
this.collection = collection;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally define a target database if different from the current one.
|
||||
*
|
||||
* @param database must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder inDatabase(String database) {
|
||||
|
||||
this.database = database;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the target to store results in.
|
||||
*
|
||||
* @param into must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder into(MergeOperationTarget into) {
|
||||
|
||||
this.database = into.database;
|
||||
this.collection = into.collection;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the target to store results in.
|
||||
*
|
||||
* @param target must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder target(MergeOperationTarget target) {
|
||||
return into(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a single field or multiple fields that act as a unique identifier for a document. The identifier
|
||||
* determines if a results document matches an already existing document in the output collection. <br />
|
||||
* The aggregation results documents must contain the field(s) specified via {@code on}, unless it's the {@code _id}
|
||||
* field.
|
||||
*
|
||||
* @param fields must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder on(String... fields) {
|
||||
return id(UniqueMergeId.ofIdFields(fields));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the identifier that determines if a results document matches an already existing document in the output
|
||||
* collection.
|
||||
*
|
||||
* @param id must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder id(UniqueMergeId id) {
|
||||
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose the variables defined by {@link Let} to the {@link WhenDocumentsMatch#updateWith(Aggregation) update
|
||||
* aggregation}.
|
||||
*
|
||||
* @param let the variable expressions
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder let(Let let) {
|
||||
|
||||
this.let = let;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose the variables defined by {@link Let} to the {@link WhenDocumentsMatch#updateWith(Aggregation) update
|
||||
* aggregation}.
|
||||
*
|
||||
* @param let the variable expressions
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder exposeVariablesOf(Let let) {
|
||||
return let(let);
|
||||
}
|
||||
|
||||
/**
|
||||
* The action to take place when documents already exist in the target collection.
|
||||
*
|
||||
* @param whenMatched must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder whenMatched(WhenDocumentsMatch whenMatched) {
|
||||
|
||||
this.whenMatched = whenMatched;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The action to take place when documents already exist in the target collection.
|
||||
*
|
||||
* @param whenMatched must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder whenDocumentsMatch(WhenDocumentsMatch whenMatched) {
|
||||
return whenMatched(whenMatched);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link Aggregation action} to take place when documents already exist in the target collection.
|
||||
*
|
||||
* @param aggregation must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder whenDocumentsMatchApply(Aggregation aggregation) {
|
||||
return whenMatched(WhenDocumentsMatch.updateWith(aggregation));
|
||||
}
|
||||
|
||||
/**
|
||||
* The action to take place when documents do not already exist in the target collection.
|
||||
*
|
||||
* @param whenNotMatched must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder whenNotMatched(WhenDocumentsDontMatch whenNotMatched) {
|
||||
|
||||
this.whenNotMatched = whenNotMatched;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The action to take place when documents do not already exist in the target collection.
|
||||
*
|
||||
* @param whenNotMatched must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public MergeOperationBuilder whenDocumentsDontMatch(WhenDocumentsDontMatch whenNotMatched) {
|
||||
return whenNotMatched(whenNotMatched);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return new instance of {@link MergeOperation}.
|
||||
*/
|
||||
public MergeOperation build() {
|
||||
return new MergeOperation(new MergeOperationTarget(database, collection), id != null ? id : UniqueMergeId.id(),
|
||||
let, whenMatched, whenNotMatched);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,16 +15,10 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.aggregation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
|
||||
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
|
||||
import org.springframework.data.mongodb.core.aggregation.SetOperation.FieldAppender.ValueAppender;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -41,10 +35,7 @@ import org.springframework.lang.Nullable;
|
||||
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/set/">MongoDB Aggregation Framework:
|
||||
* $set</a>
|
||||
*/
|
||||
public class SetOperation implements InheritsFieldsAggregationOperation {
|
||||
|
||||
private Map<Object, Object> valueMap;
|
||||
private ExposedFields exposedFields = ExposedFields.empty();
|
||||
public class SetOperation extends DocumentEnhancingOperation {
|
||||
|
||||
/**
|
||||
* Create new instance of {@link SetOperation} adding map keys as exposed fields.
|
||||
@@ -52,11 +43,7 @@ public class SetOperation implements InheritsFieldsAggregationOperation {
|
||||
* @param source must not be {@literal null}.
|
||||
*/
|
||||
private SetOperation(Map<Object, Object> source) {
|
||||
|
||||
this.valueMap = new LinkedHashMap<>(source);
|
||||
for (Object key : source.keySet()) {
|
||||
this.exposedFields = add(key);
|
||||
}
|
||||
super(source);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +84,7 @@ public class SetOperation implements InheritsFieldsAggregationOperation {
|
||||
*/
|
||||
public SetOperation set(Object field, Object value) {
|
||||
|
||||
LinkedHashMap<Object, Object> target = new LinkedHashMap<>(this.valueMap);
|
||||
LinkedHashMap<Object, Object> target = new LinkedHashMap<>(getValueMap());
|
||||
target.put(field, value);
|
||||
|
||||
return new SetOperation(target);
|
||||
@@ -109,73 +96,12 @@ public class SetOperation implements InheritsFieldsAggregationOperation {
|
||||
* @return new instance of {@link FieldAppender}.
|
||||
*/
|
||||
public FieldAppender and() {
|
||||
return new FieldAppender(this.valueMap);
|
||||
return new FieldAppender(getValueMap());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
|
||||
*/
|
||||
@Override
|
||||
public Document toDocument(AggregationOperationContext context) {
|
||||
|
||||
InheritingExposedFieldsAggregationOperationContext operationContext = new InheritingExposedFieldsAggregationOperationContext(
|
||||
exposedFields, context);
|
||||
|
||||
if (valueMap.size() == 1) {
|
||||
return context
|
||||
.getMappedObject(new Document("$set", toSetEntry(valueMap.entrySet().iterator().next(), operationContext)));
|
||||
}
|
||||
|
||||
Document $set = new Document();
|
||||
valueMap.entrySet().stream().map(it -> toSetEntry(it, operationContext)).forEach($set::putAll);
|
||||
return context.getMappedObject(new Document("$set", $set));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields()
|
||||
*/
|
||||
@Override
|
||||
public ExposedFields getFields() {
|
||||
return exposedFields;
|
||||
}
|
||||
|
||||
private ExposedFields add(Object field) {
|
||||
|
||||
if (field instanceof Field) {
|
||||
return exposedFields.and(new ExposedField((Field) field, true));
|
||||
}
|
||||
if (field instanceof String) {
|
||||
return exposedFields.and(new ExposedField(Fields.field((String) field), true));
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(String.format("Expected %s to be a field/property.", field));
|
||||
}
|
||||
|
||||
private static Document toSetEntry(Entry<Object, Object> entry, AggregationOperationContext context) {
|
||||
|
||||
String field = entry.getKey() instanceof String ? context.getReference((String) entry.getKey()).getRaw()
|
||||
: context.getReference((Field) entry.getKey()).getRaw();
|
||||
|
||||
Object value = computeValue(entry.getValue(), context);
|
||||
|
||||
return new Document(field, value);
|
||||
}
|
||||
|
||||
private static Object computeValue(Object value, AggregationOperationContext context) {
|
||||
|
||||
if (value instanceof Field) {
|
||||
return context.getReference((Field) value).toString();
|
||||
}
|
||||
if (value instanceof AggregationExpression) {
|
||||
return ((AggregationExpression) value).toDocument(context);
|
||||
}
|
||||
if (value instanceof Collection) {
|
||||
return ((Collection) value).stream().map(it -> computeValue(it, context)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return value;
|
||||
protected String mongoOperator() {
|
||||
return "$set";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -127,6 +127,15 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio
|
||||
return Fields.fields(fields.toArray(new String[0]));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#continueOnMissingFieldReference()
|
||||
*/
|
||||
@Override
|
||||
public AggregationOperationContext continueOnMissingFieldReference() {
|
||||
return new RelaxedTypeBasedAggregationOperationContext(type, mappingContext, mapper);
|
||||
}
|
||||
|
||||
protected FieldReference getReferenceFor(Field field) {
|
||||
|
||||
PersistentPropertyPath<MongoPersistentProperty> propertyPath = mappingContext
|
||||
|
||||
@@ -296,7 +296,7 @@ public class VariableOperators {
|
||||
return toLet(ExposedFields.synthetic(Fields.fields(getVariableNames())), context);
|
||||
}
|
||||
|
||||
private String[] getVariableNames() {
|
||||
String[] getVariableNames() {
|
||||
|
||||
String[] varNames = new String[this.vars.size()];
|
||||
for (int i = 0; i < this.vars.size(); i++) {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2020 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.aggregation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
|
||||
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
|
||||
import org.springframework.data.mongodb.core.convert.QueryMapper;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class AddFieldsOperationUnitTests {
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void raisesErrorOnNullField() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new AddFieldsOperation(null, "value"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void rendersFieldReferenceCorrectly() {
|
||||
|
||||
assertThat(new AddFieldsOperation("name", "value").toPipelineStages(contextFor(Scores.class)))
|
||||
.containsExactly(Document.parse("{\"$addFields\" : {\"name\":\"value\"}}"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void rendersMultipleEntriesCorrectly() {
|
||||
|
||||
assertThat(new AddFieldsOperation("name", "value").addField("field-2", "value2")
|
||||
.toPipelineStages(contextFor(Scores.class)))
|
||||
.containsExactly(Document.parse("{\"$addFields\" : {\"name\":\"value\", \"field-2\":\"value2\"}}"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void rendersMappedFieldReferenceCorrectly() {
|
||||
|
||||
assertThat(new AddFieldsOperation("student", "value").toPipelineStages(contextFor(ScoresWithMappedField.class)))
|
||||
.containsExactly(Document.parse("{\"$addFields\" : {\"student_name\":\"value\"}}"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void rendersNestedMappedFieldReferenceCorrectly() {
|
||||
|
||||
assertThat(new AddFieldsOperation("scoresWithMappedField.student", "value")
|
||||
.toPipelineStages(contextFor(ScoresWrapper.class)))
|
||||
.containsExactly(Document.parse("{\"$addFields\" : {\"scoresWithMappedField.student_name\":\"value\"}}"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void rendersTargetValueFieldReferenceCorrectly() {
|
||||
|
||||
assertThat(new AddFieldsOperation("name", Fields.field("value")).toPipelineStages(contextFor(Scores.class)))
|
||||
.containsExactly(Document.parse("{\"$addFields\" : {\"name\":\"$value\"}}"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void rendersMappedTargetValueFieldReferenceCorrectly() {
|
||||
|
||||
assertThat(new AddFieldsOperation("student", Fields.field("homework"))
|
||||
.toPipelineStages(contextFor(ScoresWithMappedField.class)))
|
||||
.containsExactly(Document.parse("{\"$addFields\" : {\"student_name\":\"$home_work\"}}"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void rendersNestedMappedTargetValueFieldReferenceCorrectly() {
|
||||
|
||||
assertThat(new AddFieldsOperation("scoresWithMappedField.student", Fields.field("scoresWithMappedField.homework"))
|
||||
.toPipelineStages(contextFor(ScoresWrapper.class)))
|
||||
.containsExactly(Document.parse(
|
||||
"{\"$addFields\" : {\"scoresWithMappedField.student_name\":\"$scoresWithMappedField.home_work\"}}"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void rendersTargetValueExpressionCorrectly() {
|
||||
|
||||
assertThat(AddFieldsOperation.builder().addField("totalHomework")
|
||||
.withValueOf(ArithmeticOperators.valueOf("homework").sum()).build().toPipelineStages(contextFor(Scores.class)))
|
||||
.containsExactly(Document.parse("{\"$addFields\" : {\"totalHomework\": { \"$sum\" : \"$homework\" }}}"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void exposesFieldsCorrectly() {
|
||||
|
||||
ExposedFields fields = AddFieldsOperation.builder().addField("totalHomework").withValue("A+") //
|
||||
.addField("totalQuiz").withValue("B-") //
|
||||
.build().getFields();
|
||||
|
||||
assertThat(fields.getField("totalHomework")).isNotNull();
|
||||
assertThat(fields.getField("totalQuiz")).isNotNull();
|
||||
assertThat(fields.getField("does-not-exist")).isNull();
|
||||
}
|
||||
|
||||
private static AggregationOperationContext contextFor(@Nullable Class<?> type) {
|
||||
|
||||
if (type == null) {
|
||||
return Aggregation.DEFAULT_CONTEXT;
|
||||
}
|
||||
|
||||
MappingMongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE,
|
||||
new MongoMappingContext());
|
||||
mongoConverter.afterPropertiesSet();
|
||||
|
||||
return new TypeBasedAggregationOperationContext(type, mongoConverter.getMappingContext(),
|
||||
new QueryMapper(mongoConverter)).continueOnMissingFieldReference();
|
||||
}
|
||||
|
||||
static class Scores {
|
||||
|
||||
String student;
|
||||
List<Integer> homework;
|
||||
}
|
||||
|
||||
static class ScoresWithMappedField {
|
||||
|
||||
@Field("student_name") String student;
|
||||
@Field("home_work") List<Integer> homework;
|
||||
}
|
||||
|
||||
static class ScoresWrapper {
|
||||
|
||||
Scores scores;
|
||||
ScoresWithMappedField scoresWithMappedField;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2020 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.aggregation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
|
||||
import static org.springframework.data.mongodb.core.aggregation.MergeOperation.*;
|
||||
import static org.springframework.data.mongodb.core.aggregation.MergeOperation.WhenDocumentsMatch.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum;
|
||||
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
|
||||
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
|
||||
import org.springframework.data.mongodb.core.convert.QueryMapper;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class MergeOperationUnitTests {
|
||||
|
||||
private static final String OUT_COLLECTION = "target-collection";
|
||||
private static final String OUT_DB = "target-db";
|
||||
|
||||
private static final Document OUT = new Document("db", OUT_DB).append("coll", OUT_COLLECTION);
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void justCollection() {
|
||||
|
||||
assertThat(mergeInto(OUT_COLLECTION).toDocument(DEFAULT_CONTEXT)).isEqualTo(new Document("$merge", OUT_COLLECTION));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void collectionInDatabase() {
|
||||
|
||||
assertThat(merge().intoCollection(OUT_COLLECTION).inDatabase("target-db").build().toDocument(DEFAULT_CONTEXT))
|
||||
.isEqualTo(new Document("$merge", new Document("into", OUT)));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void singleOn() {
|
||||
|
||||
assertThat(merge().intoCollection(OUT_COLLECTION).on("id-field").build().toDocument(DEFAULT_CONTEXT))
|
||||
.isEqualTo(new Document("$merge", new Document("into", OUT_COLLECTION).append("on", "id-field")));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void multipleOn() {
|
||||
|
||||
assertThat(merge().intoCollection(OUT_COLLECTION).on("field-1", "field-2").build().toDocument(DEFAULT_CONTEXT))
|
||||
.isEqualTo(new Document("$merge",
|
||||
new Document("into", OUT_COLLECTION).append("on", Arrays.asList("field-1", "field-2"))));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void collectionAndSimpleArgs() {
|
||||
|
||||
assertThat(merge().intoCollection(OUT_COLLECTION).on("_id").whenMatched(replaceDocument())
|
||||
.whenNotMatched(WhenDocumentsDontMatch.insertNewDocument()).build().toDocument(DEFAULT_CONTEXT))
|
||||
.isEqualTo(new Document("$merge", new Document("into", OUT_COLLECTION).append("on", "_id")
|
||||
.append("whenMatched", "replace").append("whenNotMatched", "insert")));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
void whenMatchedWithAggregation() {
|
||||
|
||||
String expected = "{ \"$merge\" : {\"into\": \"" + OUT_COLLECTION + "\", \"whenMatched\": ["
|
||||
+ "{ \"$addFields\" : {" //
|
||||
+ "\"thumbsup\": { \"$sum\":[ \"$thumbsup\", \"$$new.thumbsup\" ] },"
|
||||
+ "\"thumbsdown\": { \"$sum\": [ \"$thumbsdown\", \"$$new.thumbsdown\" ] } } } ]" //
|
||||
+ "} }";
|
||||
|
||||
Aggregation update = Aggregation
|
||||
.newAggregation(AddFieldsOperation.addField("thumbsup").withValueOf(Sum.sumOf("thumbsup").and("$$new.thumbsup"))
|
||||
.addField("thumbsdown").withValueOf(Sum.sumOf("thumbsdown").and("$$new.thumbsdown")).build());
|
||||
|
||||
assertThat(
|
||||
merge().intoCollection(OUT_COLLECTION).whenDocumentsMatchApply(update).build().toDocument(DEFAULT_CONTEXT))
|
||||
.isEqualTo(Document.parse(expected));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2363
|
||||
public void mapsFieldNames() {
|
||||
|
||||
assertThat(merge().intoCollection("newrestaurants").on("date", "postCode").build()
|
||||
.toDocument(contextFor(Restaurant.class))).isEqualTo(
|
||||
Document.parse("{ \"$merge\": { \"into\": \"newrestaurants\", \"on\": [ \"date\", \"post_code\" ] } }"));
|
||||
}
|
||||
|
||||
private static AggregationOperationContext contextFor(@Nullable Class<?> type) {
|
||||
|
||||
if (type == null) {
|
||||
return Aggregation.DEFAULT_CONTEXT;
|
||||
}
|
||||
|
||||
MappingMongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE,
|
||||
new MongoMappingContext());
|
||||
mongoConverter.afterPropertiesSet();
|
||||
|
||||
return new TypeBasedAggregationOperationContext(type, mongoConverter.getMappingContext(),
|
||||
new QueryMapper(mongoConverter)).continueOnMissingFieldReference();
|
||||
}
|
||||
|
||||
static class Restaurant {
|
||||
|
||||
@Field("post_code") String postCode;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
* Removal of `_id` flattening for composite Id's when using `MongoTemplate` aggregations.
|
||||
* Apply pagination when using GridFS `find(Query)`.
|
||||
* <<sharding,Sharding key derivation>> via `@Sharded`.
|
||||
* `$merge` and `$addFields` aggregation pipeline stages.
|
||||
|
||||
[[new-features.2-2-0]]
|
||||
== What's New in Spring Data MongoDB 2.2
|
||||
|
||||
Reference in New Issue
Block a user