Skip to content

Commit b7a0b1d

Browse files
mp911dechristophstrobl
authored andcommitted
DATAMONGO-1552 - Add $bucketAuto aggregation stage.
Original Pull Request: spring-projects#426
1 parent 5c5c616 commit b7a0b1d

File tree

6 files changed

+496
-4
lines changed

6 files changed

+496
-4
lines changed

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ public static OutOperation out(String outCollectionName) {
417417
}
418418

419419
/**
420-
* Creates a new {@link BucketOperation} using given {@literal groupByField}.
420+
* Creates a new {@link BucketOperation} given {@literal groupByField}.
421421
*
422422
* @param groupByField must not be {@literal null} or empty.
423423
* @return
@@ -427,7 +427,7 @@ public static BucketOperation bucket(String groupByField) {
427427
}
428428

429429
/**
430-
* Creates a new {@link BucketOperation} using given {@link AggregationExpression group-by expression}.
430+
* Creates a new {@link BucketOperation} given {@link AggregationExpression group-by expression}.
431431
*
432432
* @param groupByExpression must not be {@literal null}.
433433
* @return
@@ -436,6 +436,28 @@ public static BucketOperation bucket(AggregationExpression groupByExpression) {
436436
return new BucketOperation(groupByExpression);
437437
}
438438

439+
/**
440+
* Creates a new {@link BucketAutoOperation} given {@literal groupByField}.
441+
*
442+
* @param groupByField must not be {@literal null} or empty.
443+
* @param buckets number of buckets, must be a positive integer.
444+
* @return
445+
*/
446+
public static BucketAutoOperation bucketAuto(String groupByField, int buckets) {
447+
return new BucketAutoOperation(field(groupByField), buckets);
448+
}
449+
450+
/**
451+
* Creates a new {@link BucketAutoOperation} given {@link AggregationExpression group-by expression}.
452+
*
453+
* @param groupByExpression must not be {@literal null}.
454+
* @param buckets number of buckets, must be a positive integer.
455+
* @return
456+
*/
457+
public static BucketAutoOperation bucketAuto(AggregationExpression groupByExpression, int buckets) {
458+
return new BucketAutoOperation(groupByExpression, buckets);
459+
}
460+
439461
/**
440462
* Creates a new {@link LookupOperation}.
441463
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*
2+
* Copyright 2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.aggregation;
17+
18+
import org.springframework.data.mongodb.core.aggregation.BucketAutoOperation.BucketAutoOperationOutputBuilder;
19+
import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder;
20+
import org.springframework.util.Assert;
21+
22+
import org.bson.Document;
23+
24+
/**
25+
* Encapsulates the aggregation framework {@code $bucketAuto}-operation.
26+
* <p>
27+
* Bucket stage is typically used with {@link Aggregation} and {@code $facet}. Categorizes incoming documents into a
28+
* specific number of groups, called buckets, based on a specified expression. Bucket boundaries are automatically
29+
* determined in an attempt to evenly distribute the documents into the specified number of buckets.
30+
* <p>
31+
* We recommend to use the static factory method {@link Aggregation#bucketAuto(String, int)} instead of creating instances of
32+
* this class directly.
33+
*
34+
* @see http://docs.mongodb.org/manual/reference/aggregation/bucketAuto/
35+
* @see BucketOperationSupport
36+
* @author Mark Paluch
37+
* @since 1.10
38+
*/
39+
public class BucketAutoOperation extends BucketOperationSupport<BucketAutoOperation, BucketAutoOperationOutputBuilder>
40+
implements FieldsExposingAggregationOperation {
41+
42+
private final int buckets;
43+
private final String granularity;
44+
45+
/**
46+
* Creates a new {@link BucketAutoOperation} given a {@link Field group-by field}.
47+
*
48+
* @param groupByField must not be {@literal null}.
49+
* @param buckets number of buckets, must be a positive integer.
50+
*/
51+
public BucketAutoOperation(Field groupByField, int buckets) {
52+
53+
super(groupByField);
54+
55+
Assert.isTrue(buckets > 0, "Number of buckets must be greater 0!");
56+
57+
this.buckets = buckets;
58+
this.granularity = null;
59+
}
60+
61+
/**
62+
* Creates a new {@link BucketAutoOperation} given a {@link AggregationExpression group-by expression}.
63+
*
64+
* @param groupByExpression must not be {@literal null}.
65+
* @param buckets number of buckets, must be a positive integer.
66+
*/
67+
public BucketAutoOperation(AggregationExpression groupByExpression, int buckets) {
68+
69+
super(groupByExpression);
70+
71+
Assert.isTrue(buckets > 0, "Number of buckets must be greater 0!");
72+
73+
this.buckets = buckets;
74+
this.granularity = null;
75+
}
76+
77+
private BucketAutoOperation(BucketAutoOperation bucketOperation, Outputs outputs) {
78+
79+
super(bucketOperation, outputs);
80+
81+
this.buckets = bucketOperation.buckets;
82+
this.granularity = bucketOperation.granularity;
83+
}
84+
85+
private BucketAutoOperation(BucketAutoOperation bucketOperation, int buckets, String granularity) {
86+
87+
super(bucketOperation);
88+
89+
this.buckets = buckets;
90+
this.granularity = granularity;
91+
}
92+
93+
/* (non-Javadoc)
94+
* @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
95+
*/
96+
@Override
97+
public Document toDocument(AggregationOperationContext context) {
98+
99+
Document options = new Document();
100+
101+
options.put("buckets", buckets);
102+
103+
if (granularity != null) {
104+
options.put("granularity", granularity);
105+
}
106+
107+
options.putAll(super.toDocument(context));
108+
109+
return new Document("$bucketAuto", options);
110+
}
111+
112+
/**
113+
* Configures a number of bucket {@literal buckets} and return a new {@link BucketAutoOperation}.
114+
*
115+
* @param buckets must be a positive number.
116+
* @return
117+
*/
118+
public BucketAutoOperation withBuckets(int buckets) {
119+
120+
Assert.isTrue(buckets > 0, "Number of buckets must be greater 0!");
121+
return new BucketAutoOperation(this, buckets, granularity);
122+
}
123+
124+
/**
125+
* Configures {@literal granularity} that specifies the preferred number series to use to ensure that the calculated
126+
* boundary edges end on preferred round numbers or their powers of 10 and return a new {@link BucketAutoOperation}.
127+
*
128+
* @param granularity must not be {@literal null}.
129+
* @return
130+
*/
131+
public BucketAutoOperation withGranularity(Granularity granularity) {
132+
133+
Assert.notNull(granularity, "Granularity must not be null!");
134+
135+
return new BucketAutoOperation(this, buckets, granularity.toMongoGranularity());
136+
}
137+
138+
/* (non-Javadoc)
139+
* @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#newBucketOperation(org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.Outputs)
140+
*/
141+
@Override
142+
protected BucketAutoOperation newBucketOperation(Outputs outputs) {
143+
return new BucketAutoOperation(this, outputs);
144+
}
145+
146+
/* (non-Javadoc)
147+
* @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#andOutputExpression(java.lang.String, java.lang.Object[])
148+
*/
149+
@Override
150+
public ExpressionBucketAutoOperationBuilder andOutputExpression(String expression, Object... params) {
151+
return new ExpressionBucketAutoOperationBuilder(expression, this, params);
152+
}
153+
154+
/* (non-Javadoc)
155+
* @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#andOutput(org.springframework.data.mongodb.core.aggregation.AggregationExpression)
156+
*/
157+
@Override
158+
public BucketAutoOperationOutputBuilder andOutput(AggregationExpression expression) {
159+
return new BucketAutoOperationOutputBuilder(expression, this);
160+
}
161+
162+
/* (non-Javadoc)
163+
* @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#andOutput(java.lang.String)
164+
*/
165+
@Override
166+
public BucketAutoOperationOutputBuilder andOutput(String fieldName) {
167+
return new BucketAutoOperationOutputBuilder(Fields.field(fieldName), this);
168+
}
169+
170+
/**
171+
* {@link OutputBuilder} implementation for {@link BucketAutoOperation}.
172+
*/
173+
public static class BucketAutoOperationOutputBuilder
174+
extends OutputBuilder<BucketAutoOperationOutputBuilder, BucketAutoOperation> {
175+
176+
/**
177+
* Creates a new {@link BucketAutoOperationOutputBuilder} fot the given value and {@link BucketAutoOperation}.
178+
*
179+
* @param value must not be {@literal null}.
180+
* @param operation must not be {@literal null}.
181+
*/
182+
protected BucketAutoOperationOutputBuilder(Object value, BucketAutoOperation operation) {
183+
super(value, operation);
184+
}
185+
186+
/* (non-Javadoc)
187+
* @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder#apply(org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OperationOutput)
188+
*/
189+
@Override
190+
protected BucketAutoOperationOutputBuilder apply(OperationOutput operationOutput) {
191+
return new BucketAutoOperationOutputBuilder(operationOutput, this.operation);
192+
}
193+
}
194+
195+
/**
196+
* {@link ExpressionBucketOperationBuilderSupport} implementation for {@link BucketAutoOperation} using SpEL
197+
* expression based {@link Output}.
198+
*
199+
* @author Mark Paluch
200+
*/
201+
public static class ExpressionBucketAutoOperationBuilder
202+
extends ExpressionBucketOperationBuilderSupport<BucketAutoOperationOutputBuilder, BucketAutoOperation> {
203+
204+
/**
205+
* Creates a new {@link ExpressionBucketAutoOperationBuilder} for the given value, {@link BucketAutoOperation} and
206+
* parameters.
207+
*
208+
* @param expression must not be {@literal null}.
209+
* @param operation must not be {@literal null}.
210+
* @param parameters
211+
*/
212+
protected ExpressionBucketAutoOperationBuilder(String expression, BucketAutoOperation operation,
213+
Object[] parameters) {
214+
super(expression, operation, parameters);
215+
}
216+
217+
/* (non-Javadoc)
218+
* @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder#apply(org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OperationOutput)
219+
*/
220+
@Override
221+
protected BucketAutoOperationOutputBuilder apply(OperationOutput operationOutput) {
222+
return new BucketAutoOperationOutputBuilder(operationOutput, this.operation);
223+
}
224+
}
225+
226+
/**
227+
* @author Mark Paluch
228+
*/
229+
public static interface Granularity {
230+
231+
/**
232+
* @return a String that represents a MongoDB granularity to be used with {@link BucketAutoOperation}.
233+
*/
234+
String toMongoGranularity();
235+
}
236+
237+
/**
238+
* Supported MongoDB granularities.
239+
*
240+
* @see https://en.wikipedia.org/wiki/Preferred_number
241+
* @see https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/#granularity
242+
* @author Mark Paluch
243+
*/
244+
public enum Granularities implements Granularity {
245+
246+
R5, R10, R20, R40, R80, //
247+
248+
SERIES_1_2_5("1-2-5"), //
249+
250+
E6, E12, E24, E48, E96, E192, //
251+
252+
POWERSOF2;
253+
254+
final String granularity;
255+
256+
Granularities() {
257+
this.granularity = name();
258+
}
259+
260+
Granularities(String granularity) {
261+
this.granularity = granularity;
262+
}
263+
264+
/* (non-Javadoc)
265+
* @see org.springframework.data.mongodb.core.aggregation.GranularitytoMongoGranularity()
266+
*/
267+
@Override
268+
public String toMongoGranularity() {
269+
return granularity;
270+
}
271+
}
272+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ public abstract static class OutputBuilder<B extends OutputBuilder<B, T>, T exte
221221
* @param value must not be {@literal null}.
222222
* @param operation must not be {@literal null}.
223223
*/
224-
public OutputBuilder(Object value, T operation) {
224+
protected OutputBuilder(Object value, T operation) {
225225

226226
Assert.notNull(value, "Value must not be null or empty!");
227227
Assert.notNull(operation, "ProjectionOperation must not be null!");
@@ -432,6 +432,11 @@ private Outputs(Collection<Output> current, Output output) {
432432
*/
433433
protected ExposedFields asExposedFields() {
434434

435+
// The count field is included by default when the output is not specified.
436+
if (isEmpty()) {
437+
return ExposedFields.from(new ExposedField("count", true));
438+
}
439+
435440
ExposedFields fields = ExposedFields.from();
436441

437442
for (Output output : outputs) {

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@
6262
import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ConditionalOperators;
6363
import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let;
6464
import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let.ExpressionVariable;
65+
import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Multiply;
6566
import org.springframework.data.mongodb.core.aggregation.AggregationTests.CarDescriptor.Entry;
67+
import org.springframework.data.mongodb.core.aggregation.BucketAutoOperation.Granularities;
6668
import org.springframework.data.mongodb.core.index.GeospatialIndex;
6769
import org.springframework.data.mongodb.core.query.Criteria;
6870
import org.springframework.data.mongodb.core.query.NearQuery;
@@ -1702,6 +1704,43 @@ public void bucketShouldCollectDocumentsIntoABucket() {
17021704
assertThat((Double) bound100.get("sum"), is(closeTo(3672.9, 0.1)));
17031705
}
17041706

1707+
/**
1708+
* @see DATAMONGO-1552
1709+
*/
1710+
@Test
1711+
public void bucketAutoShouldCollectDocumentsIntoABucket() {
1712+
1713+
assumeTrue(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_FOUR));
1714+
1715+
Art a1 = Art.builder().id(1).title("The Pillars of Society").artist("Grosz").year(1926).price(199.99).build();
1716+
Art a2 = Art.builder().id(2).title("Melancholy III").artist("Munch").year(1902).price(280.00).build();
1717+
Art a3 = Art.builder().id(3).title("Dancer").artist("Miro").year(1925).price(76.04).build();
1718+
Art a4 = Art.builder().id(4).title("The Great Wave off Kanagawa").artist("Hokusai").price(167.30).build();
1719+
1720+
mongoTemplate.insert(Arrays.asList(a1, a2, a3, a4), Art.class);
1721+
1722+
TypedAggregation<Art> aggregation = newAggregation(Art.class, //
1723+
bucketAuto(Multiply.valueOf("price").multiplyBy(10), 3) //
1724+
.withGranularity(Granularities.E12) //
1725+
.andOutputCount().as("count") //
1726+
.andOutput("title").push().as("titles") //
1727+
.andOutputExpression("price * 10").sum().as("sum"));
1728+
1729+
AggregationResults<Document> result = mongoTemplate.aggregate(aggregation, Document.class);
1730+
assertThat(result.getMappedResults().size(), is(3));
1731+
1732+
// { "min" : 680.0 , "max" : 820.0 , "count" : 1 , "titles" : [ "Dancer"] , "sum" : 760.4000000000001}
1733+
Document bound0 = result.getMappedResults().get(0);
1734+
assertThat(bound0, isBsonObject().containing("count", 1).containing("titles.[0]", "Dancer").containing("min", 680.0)
1735+
.containing("max"));
1736+
1737+
// { "min" : 820.0 , "max" : 1800.0 , "count" : 1 , "titles" : [ "The Great Wave off Kanagawa"] , "sum" : 1673.0}
1738+
Document bound1 = result.getMappedResults().get(1);
1739+
assertThat(bound1, isBsonObject().containing("count", 1).containing("min", 820.0));
1740+
assertThat((List<String>) bound1.get("titles"), hasItems("The Great Wave off Kanagawa"));
1741+
assertThat((Double) bound1.get("sum"), is(closeTo(1673.0, 0.1)));
1742+
}
1743+
17051744
private void createUsersWithReferencedPersons() {
17061745

17071746
mongoTemplate.dropCollection(User.class);

0 commit comments

Comments
 (0)