Skip to content

Commit 2c4377c

Browse files
mp911dechristophstrobl
authored andcommitted
DATAMONGO-1552 - Add $bucketAuto aggregation stage.
Original Pull Request: #426
1 parent e992d81 commit 2c4377c

File tree

6 files changed

+500
-5
lines changed

6 files changed

+500
-5
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
@@ -419,7 +419,7 @@ public static OutOperation out(String outCollectionName) {
419419
}
420420

421421
/**
422-
* Creates a new {@link BucketOperation} using given {@literal groupByField}.
422+
* Creates a new {@link BucketOperation} given {@literal groupByField}.
423423
*
424424
* @param groupByField must not be {@literal null} or empty.
425425
* @return
@@ -429,7 +429,7 @@ public static BucketOperation bucket(String groupByField) {
429429
}
430430

431431
/**
432-
* Creates a new {@link BucketOperation} using given {@link AggregationExpression group-by expression}.
432+
* Creates a new {@link BucketOperation} given {@link AggregationExpression group-by expression}.
433433
*
434434
* @param groupByExpression must not be {@literal null}.
435435
* @return
@@ -438,6 +438,28 @@ public static BucketOperation bucket(AggregationExpression groupByExpression) {
438438
return new BucketOperation(groupByExpression);
439439
}
440440

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

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
@@ -222,7 +222,7 @@ public abstract static class OutputBuilder<B extends OutputBuilder<B, T>, T exte
222222
* @param value must not be {@literal null}.
223223
* @param operation must not be {@literal null}.
224224
*/
225-
public OutputBuilder(Object value, T operation) {
225+
protected OutputBuilder(Object value, T operation) {
226226

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

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

438443
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
@@ -61,7 +61,9 @@
6161
import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ConditionalOperators;
6262
import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let;
6363
import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let.ExpressionVariable;
64+
import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Multiply;
6465
import org.springframework.data.mongodb.core.aggregation.AggregationTests.CarDescriptor.Entry;
66+
import org.springframework.data.mongodb.core.aggregation.BucketAutoOperation.Granularities;
6567
import org.springframework.data.mongodb.core.index.GeospatialIndex;
6668
import org.springframework.data.mongodb.core.mapping.Document;
6769
import org.springframework.data.mongodb.core.query.Criteria;
@@ -1708,6 +1710,43 @@ public void bucketShouldCollectDocumentsIntoABucket() {
17081710
assertThat((Double) bound100.get("sum"), is(closeTo(3672.9, 0.1)));
17091711
}
17101712

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

17131752
mongoTemplate.dropCollection(User.class);

0 commit comments

Comments
 (0)