From 9a13a3fce4da46566d0f2fb518d855e92ddc0a13 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 8 Jan 2018 15:33:33 +0100 Subject: [PATCH] DATAMONGO-1553 - Polishing. Convert spaces to tabs. Reorder methods. Add tests, nullability annotations, author tags and slightly rearrange documentation. Migrate tests to AssertJ. Extend year range in license headers. Original pull request: #519. --- .../mongodb/core/aggregation/Aggregation.java | 44 +++++----- .../aggregation/SortByCountOperation.java | 82 ++++++++++--------- .../aggregation/FacetOperationUnitTests.java | 54 ++++++------ .../SortByCountOperationUnitTests.java | 50 +++++++++++ src/main/asciidoc/reference/mongodb.adoc | 40 +++++---- 5 files changed, 168 insertions(+), 102 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperationUnitTests.java 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 23ea78cd1..1b3b00f28 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2017 the original author or authors. + * Copyright 2013-2018 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. @@ -358,6 +358,28 @@ public class Aggregation { return new SortOperation(Sort.by(direction, fields)); } + /** + * Creates a new {@link SortByCountOperation} given {@literal groupByField}. + * + * @param field must not be {@literal null} or empty. + * @return + * @since 2.1 + */ + public static SortByCountOperation sortByCount(String field) { + return new SortByCountOperation(field(field)); + } + + /** + * Creates a new {@link SortByCountOperation} given {@link AggregationExpression group and sort expression}. + * + * @param groupAndSortExpression must not be {@literal null}. + * @return + * @since 2.1 + */ + public static SortByCountOperation sortByCount(AggregationExpression groupAndSortExpression) { + return new SortByCountOperation(groupAndSortExpression); + } + /** * Creates a new {@link SkipOperation} skipping the given number of elements. * @@ -481,26 +503,6 @@ public class Aggregation { return new BucketAutoOperation(groupByExpression, buckets); } - /** - * Creates a new {@link SortByCountOperation} given {@literal groupByField} - * - * @param groupByField must not be {@literal null} or empty. - * @return - */ - public static SortByCountOperation sortByCount(String groupByField) { - return new SortByCountOperation(field(groupByField)); - } - - /** - * Creates a new {@link SortByCountOperation} given {@link AggregationExpression group-by expression}. - * - * @param groupByExpression must not be {@literal null}. - * @return - */ - public static SortByCountOperation sortByCount(AggregationExpression groupByExpression) { - return new SortByCountOperation(groupByExpression); - } - /** * Creates a new {@link FacetOperation}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java index d5d33ad2b..945b00015 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-2018 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. @@ -16,57 +16,65 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Encapsulates the aggregation framework {@code $sortByCount}-operation.
- * - * SortByCount stage is typically used with {@link Aggregation} and {@code $facet}. Groups incoming documents - * based on the value of a specified expression, then computes the count of documents in each distinct group.
- * + * Encapsulates the aggregation framework {@code $sortByCount}-operation. + *

+ * {@code $sortByCount} stage is typically used with {@link Aggregation} and {@code $facet}. Groups incoming documents + * based on the value of a specified expression and computes the count of documents in each distinct group. + * {@link SortByCountOperation} is equivalent to {@code { $group: { _id: , count: { $sum: 1 } } }, { $sort: + * { count: -1 } }}. + *

* We recommend to use the static factory method {@link Aggregation#sortByCount(String)} instead of creating instances * of this class directly. * - * @see https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/ - * @author Jérôme GUYON + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/ + * @author Jérôme Guyon + * @author Mark Paluch + * @since 2.1 */ public class SortByCountOperation implements AggregationOperation { - private final Field groupByField; - private final AggregationExpression groupByExpression; + private final @Nullable Field groupByField; + private final @Nullable AggregationExpression groupByExpression; + /** + * Creates a new {@link SortByCountOperation} given a {@link Field group-by field}. + * + * @param groupByField must not be {@literal null}. + */ + public SortByCountOperation(Field groupByField) { - /** - * Creates a new {@link SortByCountOperation} given a {@link Field group-by field}. - * - * @param groupByField must not be {@literal null}. - */ - public SortByCountOperation(Field groupByField) { + Assert.notNull(groupByField, "Group by field must not be null!"); - Assert.notNull(groupByField, "Group by field must not be null!"); + this.groupByField = groupByField; + this.groupByExpression = null; + } - this.groupByField = groupByField; - this.groupByExpression = null; - } + /** + * Creates a new {@link SortByCountOperation} given a {@link AggregationExpression group-by expression}. + * + * @param groupByExpression must not be {@literal null}. + */ + public SortByCountOperation(AggregationExpression groupByExpression) { - /** - * Creates a new {@link SortByCountOperation} given a {@link AggregationExpression group-by expression}. - * - * @param groupByExpression must not be {@literal null}. - */ - public SortByCountOperation(AggregationExpression groupByExpression) { + Assert.notNull(groupByExpression, "Group by expression must not be null!"); - Assert.notNull(groupByExpression, "Group by AggregationExpression must not be null!"); + this.groupByExpression = groupByExpression; + this.groupByField = null; + } - this.groupByExpression = groupByExpression; - this.groupByField = null; - } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toDocument(AggregationOperationContext context) { - @Override - public Document toDocument(AggregationOperationContext context) { - return new Document("$sortByCount", - groupByExpression == null ? context.getReference(groupByField).toString() - : groupByExpression.toDocument(context) - ); - } + return new Document("$sortByCount", groupByExpression == null ? context.getReference(groupByField).toString() + : groupByExpression.toDocument(context)); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FacetOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FacetOperationUnitTests.java index 886b6b2cc..7b84210d0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FacetOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FacetOperationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2017 the original author or authors. + * Copyright 2016-2018 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. @@ -15,14 +15,14 @@ */ package org.springframework.data.mongodb.core.aggregation; -import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; - -import org.junit.Test; -import org.springframework.data.mongodb.core.query.Criteria; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import org.bson.Document; +import org.junit.Test; +import org.springframework.data.mongodb.core.query.Criteria; /** * Unit tests for {@link FacetOperation}. @@ -34,29 +34,28 @@ import org.bson.Document; public class FacetOperationUnitTests { @Test // DATAMONGO-1552 - public void shouldRenderCorrectly() throws Exception { + public void shouldRenderCorrectly() { - FacetOperation facetOperation = new FacetOperation() - .and(match(Criteria.where("price").exists(true)), // - bucket("price") // - .withBoundaries(0, 150, 200, 300, 400) // - .withDefaultBucket("Other") // - .andOutputCount().as("count") // - .andOutput("title").push().as("titles")) // + FacetOperation facetOperation = new FacetOperation().and(match(Criteria.where("price").exists(true)), // + bucket("price") // + .withBoundaries(0, 150, 200, 300, 400) // + .withDefaultBucket("Other") // + .andOutputCount().as("count") // + .andOutput("title").push().as("titles")) // .as("categorizedByPrice") // .and(bucketAuto("year", 5)).as("categorizedByYears"); Document agg = facetOperation.toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, - is(Document.parse("{ $facet: { categorizedByPrice: [" + "{ $match: { price: { $exists: true } } }, " + assertThat(agg) + .isEqualTo(Document.parse("{ $facet: { categorizedByPrice: [" + "{ $match: { price: { $exists: true } } }, " + "{ $bucket: { boundaries: [ 0, 150, 200, 300, 400 ], groupBy: \"$price\", default: \"Other\", " + "output: { count: { $sum: 1 }, titles: { $push: \"$title\" } } } } ]," - + "categorizedByYears: [ { $bucketAuto: { buckets: 5, groupBy: \"$year\" } } ] } }"))); + + "categorizedByYears: [ { $bucketAuto: { buckets: 5, groupBy: \"$year\" } } ] } }")); } @Test // DATAMONGO-1552 - public void shouldRenderEmpty() throws Exception { + public void shouldRenderEmpty() { FacetOperation facetOperation = facet(); @@ -66,7 +65,7 @@ public class FacetOperationUnitTests { } @Test(expected = IllegalArgumentException.class) // DATAMONGO-1552 - public void shouldRejectNonExistingFields() throws Exception { + public void shouldRejectNonExistingFields() { FacetOperation facetOperation = new FacetOperation() .and(project("price"), // @@ -79,11 +78,11 @@ public class FacetOperationUnitTests { Document agg = facetOperation.toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, - is(Document.parse("{ $facet: { categorizedByPrice: [" + "{ $match: { price: { $exists: true } } }, " + assertThat(agg) + .isEqualTo(Document.parse("{ $facet: { categorizedByPrice: [" + "{ $match: { price: { $exists: true } } }, " + "{ $bucket: {boundaries: [ 0, 150, 200, 300, 400 ], groupBy: \"$price\", default: \"Other\", " + "output: { count: { $sum: 1 }, titles: { $push: \"$title\" } } } } ]," - + "categorizedByYears: [ { $bucketAuto: { buckets: 5, groupBy: \"$year\" } } ] } }"))); + + "categorizedByYears: [ { $bucketAuto: { buckets: 5, groupBy: \"$year\" } } ] } }")); } @Test // DATAMONGO-1552 @@ -97,22 +96,21 @@ public class FacetOperationUnitTests { Document agg = facetOperation.toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, - is(Document.parse("{ $facet: { categorizedByPrice: [" + "{ $project: { price: 1, name: \"$title\" } }, " + assertThat(agg).isEqualTo(Document.parse("{ $facet: { categorizedByPrice: [" + + "{ $project: { price: 1, name: \"$title\" } }, " + "{ $bucketAuto: { buckets: 5, groupBy: \"$price\", " - + "output: { titles: { $push: \"$name\" } } } } ] } }"))); + + "output: { titles: { $push: \"$name\" } } } } ] } }")); } @Test // DATAMONGO-1553 public void shouldRenderSortByCountCorrectly() { - FacetOperation facetOperation = new FacetOperation() - .and(sortByCount("country")) + FacetOperation facetOperation = new FacetOperation() // + .and(sortByCount("country")) // .as("categorizedByCountry"); Document agg = facetOperation.toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, - is(Document.parse("{ $facet: { categorizedByCountry: [{ $sortByCount: \"$country\" } ] } }"))); + assertThat(agg).containsEntry("$facet.categorizedByCountry.[0].$sortByCount", "$country"); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperationUnitTests.java new file mode 100644 index 000000000..8c7c44680 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperationUnitTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2018 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.springframework.data.mongodb.core.aggregation.Aggregation.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; + +import java.util.Arrays; + +import org.bson.Document; +import org.junit.Test; + +/** + * Unit tests for {@link SortByCountOperation}. + * + * @author Mark Paluch + */ +public class SortByCountOperationUnitTests { + + @Test // DATAMONGO-1553 + public void shouldRenderFieldCorrectly() { + + SortByCountOperation operation = sortByCount("country"); + Document result = operation.toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(result).containsEntry("$sortByCount", "$country"); + } + + @Test // DATAMONGO-1553 + public void shouldRenderExpressionCorrectly() { + + SortByCountOperation operation = sortByCount(StringOperators.valueOf("foo").substring(5)); + Document result = operation.toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(result).containsEntry("$sortByCount.$substr", Arrays.asList("$foo", 5, -1)); + } +} diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 1da380849..a4b994505 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1914,19 +1914,6 @@ More examples for project operations can be found in the `AggregationTests` clas MongoDB supports as of Version 3.4 faceted classification using the Aggregation Framework. A faceted classification uses semantic categories, either general or subject-specific, that are combined to create the full classification entry. Documents flowing through the aggregation pipeline are classificated into buckets. A multi-faceted classification enables various aggregations on the same set of input documents, without needing to retrieve the input documents multiple times. -==== SortByCount - -SortByCount operations groups incoming documents based on the value of a specified expression, then computes the count of documents in each distinct group. SortVyCount operations require a grouping field or grouping expression. They can be defined via the `sortByCount()` methods of the `Aggregate` class. - -.SortByCount operation example -==== -[source,java] ----- -// will generate { $sortByCount: "$country" } -sortByCount("country"); ----- -==== - ==== Buckets Bucket operations categorize incoming documents into groups, called buckets, based on a specified expression and bucket boundaries. Bucket operations require a grouping field or grouping expression. They can be defined via the `bucket()`/`bucketAuto()` methods of the `Aggregate` class. `BucketOperation` and `BucketAutoOperation` can expose accumulations based on aggregation expressions for input documents. The bucket operation can be extended with additional parameters through a fluent API via the `with…()` methods, the `andOutput(String)` method and aliased via the `as(String)` method. Each bucket is represented as a document in the output. @@ -1992,9 +1979,9 @@ facet(match(Criteria.where("price").exists(true)), bucketAuto("price", 5)).as("c facet(match(Criteria.where("country").exists(true)), sortByCount("country")).as("categorizedByCountry")) // will generate {$facet: {categorizedByYear: [ -// { $project: { title: 1, publicationYear: { $year: "publicationDate"}}}, -// { $bucketAuto: {groupBy: $price, buckets: 5, output: { titles: {$push:"$title"}}} -// ]}} +// { $project: { title: 1, publicationYear: { $year: "publicationDate"}}}, +// { $bucketAuto: {groupBy: $price, buckets: 5, output: { titles: {$push:"$title"}}} +// ]}} facet(project("title").and("publicationDate").extractYear().as("publicationYear"), bucketAuto("publicationYear", 5).andOutput("title").push().as("titles")) .as("categorizedByYear")) @@ -2003,6 +1990,27 @@ facet(project("title").and("publicationDate").extractYear().as("publicationYear" Note that further details regarding facet operation can be found in the http://docs.mongodb.org/manual/reference/operator/aggregation/facet/[`$facet` section] of the MongoDB Aggregation Framework reference documentation. +[[mongo.aggregation.sort-by-count]] +==== SortByCount + +Sort by count operations group incoming documents based on the value of a specified expression, then compute the count of documents in each distinct group and sort the results by count. It's a handy shortcut to apply sorting for when using <>. Sort by count operations require a grouping field or grouping expression. + +.Sort by count example +==== +[source,java] +---- +// will generate { $sortByCount: "$country" } +sortByCount("country"); +---- +==== + +A sort by count operation is equivalent to the following BSON: + +---- +{ $group: { _id: , count: { $sum: 1 } } }, +{ $sort: { count: -1 } } +---- + [[mongo.aggregation.projection.expressions]] ==== Spring Expression Support in Projection Expressions