From 4d8f5d63c7da5cc224364da4687e0c386e75632e Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 20 Mar 2018 15:29:52 +0100 Subject: [PATCH] DATAMONGO-1906 - Add SystemVariable $$REMOVE for aggregation $project stage. Add, document and make sure conditional projection in aggregation is treated correctly. Original pull request: #540. --- .../mongodb/core/aggregation/Aggregation.java | 32 +++++++++++++++++-- .../ProjectionOperationUnitTests.java | 30 +++++++++++++++++ src/main/asciidoc/reference/mongodb.adoc | 20 ++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java index 1b3b00f28..acea5e03b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java @@ -64,6 +64,33 @@ public class Aggregation { */ public static final String CURRENT = SystemVariable.CURRENT.toString(); + /** + * A variable which evaluates to the missing value. Allows for the conditional exclusion of fields. In a + * {@code $projection}, a field set to the variable {@literal REMOVE} is excluded from the output. + * + *
+	 * 
+	 * 
+	 * db.books.aggregate( [
+	 * {
+	 *     $project: {
+	 *         title: 1,
+	 *         "author.first": 1,
+	 *         "author.last" : 1,
+	 *         "author.middle": {
+	 *             $cond: {
+	 *                 if: { $eq: [ "", "$author.middle" ] },
+	 *                 then: "$$REMOVE",
+	 *                 else: "$author.middle"
+	 *             }
+	 *         }
+	 *     }
+	 * } ] )
+	 * 
+	 * 
+ */ + public static final String REMOVE = SystemVariable.REMOVE.toString(); + public static final AggregationOperationContext DEFAULT_CONTEXT = AggregationOperationRenderer.DEFAULT_CONTEXT; public static final AggregationOptions DEFAULT_OPTIONS = newAggregationOptions().build(); @@ -648,11 +675,12 @@ public class Aggregation { * Describes the system variables available in MongoDB aggregation framework pipeline expressions. * * @author Thomas Darimont - * @see Aggregation Variables + * @author Christoph Strobl + * @see Aggregation Variables. */ enum SystemVariable { - ROOT, CURRENT; + ROOT, CURRENT, REMOVE; private static final String PREFIX = "$$"; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java index 3baa77495..dec24890c 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java @@ -21,6 +21,8 @@ import static org.springframework.data.mongodb.core.aggregation.Fields.*; import static org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable.*; import static org.springframework.data.mongodb.test.util.Assertions.*; +import lombok.Data; + import java.util.Arrays; import java.util.List; @@ -212,6 +214,21 @@ public class ProjectionOperationUnitTests { assertThat((Integer) projectClause.get(Fields.UNDERSCORE_ID)).isEqualTo(0); } + @Test // DATAMONGO-1906 + public void rendersConditionalProjectionCorrectly() { + + TypedAggregation aggregation = Aggregation.newAggregation(Book.class, + Aggregation.project("title") + .and(ConditionalOperators.when(ComparisonOperators.valueOf("author.middle").equalToValue("")) + .then("$$REMOVE").otherwiseValueOf("author.middle")) + .as("author.middle")); + + Document document = aggregation.toDocument("books", Aggregation.DEFAULT_CONTEXT); + + assertThat(document).isEqualTo(Document.parse( + "{\"aggregate\" : \"books\", \"pipeline\" : [{\"$project\" : {\"title\" : 1, \"author.middle\" : {\"$cond\" : {\"if\" : {\"$eq\" : [\"$author.middle\", \"\"]}, \"then\" : \"$$REMOVE\",\"else\" : \"$author.middle\"} }}}]}")); + } + @Test // DATAMONGO-757 public void usesImplictAndExplicitFieldAliasAndIncludeExclude() { @@ -1715,4 +1732,17 @@ public class ProjectionOperationUnitTests { return (Document) fromProjectClause.get(field); } + @Data + static class Book { + String title; + Author author; + } + + @Data + static class Author { + String first; + String last; + String middle; + } + } diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 6ea8bc8ba..1508e6050 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -2565,6 +2565,26 @@ List stateStatsList = result.getMappedResults(); * This one-step aggregation uses a projection operation with the `inventory` collection. We project the `discount` field using a conditional operation for all inventory items that have a `qty` greater or equal to `250`. A second conditional projection is performed for the `description` field. We apply the description `Unspecified` to all items that either do not have a `description` field of items that have a `null` description. +As of MongoDB 3.6 it is possible to exclude fields from the projection using a conditional expression. + +.Conditional aggregation projection +==== +[source,java] +---- +TypedAggregation agg = Aggregation.newAggregation(Book.class, + project("title") + .and(ConditionalOperators.when(ComparisonOperators.valueOf("author.middle") <1> + .equalToValue("")) <2> + .then("$$REMOVE") <3> + .otherwiseValueOf("author.middle") <4> + ) + .as("author.middle")); +---- +<1> If the value of the field `author.middle` +<2> does not contain a value +<3> then use https://docs.mongodb.com/manual/reference/aggregation-variables/#variable.REMOVE[``$$REMOVE``] to exclude the field +<4> else add the field value of `author.middle`. +==== [[mongo.custom-converters]] == Overriding default mapping with custom converters