Skip to content

Commit 96068eb

Browse files
mp911dechristophstrobl
authored andcommitted
DATAMONGO-1585 - Expose synthetic fields in $project aggregation stage.
Field projections now expose their fields as synthetic simple fields. Projection aggregation stage redefines the available field set available for later aggregation stages entirely so projected fields are considered synthetic. A simple synthetic field has no target field which causes later aggregation stages to not pick up the underlying target but the exposed field name when rendering aggregation operations to Mongo documents. The change is motivated by a bug where previously an aggregation consisting of projection of an aliased field and sort caused the sort projection stage to render with the original field name instead of the aliased field. The sort did not apply any sorting since projection redefines the available field set entirely and the original field is no longer accessible. Original Pull Request: #433
1 parent 36d2e09 commit 96068eb

File tree

4 files changed

+103
-28
lines changed

4 files changed

+103
-28
lines changed

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2015 the original author or authors.
2+
* Copyright 2013-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import java.util.Collections;
2121
import java.util.List;
2222

23+
2324
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
2425
import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField;
2526
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.FieldProjection;
@@ -552,7 +553,7 @@ public ProjectionOperationBuilder mod(Number number) {
552553
/**
553554
* Generates an {@code $mod} expression that divides the value of the given field by the previously mentioned field
554555
* and returns the remainder.
555-
*
556+
*
556557
* @param fieldReference
557558
* @return
558559
*/
@@ -566,7 +567,7 @@ public ProjectionOperationBuilder size() {
566567
return project("size");
567568
}
568569

569-
/*
570+
/*
570571
* (non-Javadoc)
571572
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
572573
*/
@@ -622,6 +623,7 @@ public DBObject toDBObject(AggregationOperationContext context) {
622623
*
623624
* @author Oliver Gierke
624625
* @author Thomas Darimont
626+
* @author Mark Paluch
625627
*/
626628
static class FieldProjection extends Projection {
627629

@@ -640,7 +642,7 @@ public FieldProjection(String name, Object value) {
640642

641643
private FieldProjection(Field field, Object value) {
642644

643-
super(field);
645+
super(new ExposedField(field.getName(), true));
644646

645647
this.field = field;
646648
this.value = value;
@@ -732,7 +734,7 @@ public OperationProjection(Field field, String operation, Object[] values) {
732734
this.values = Arrays.asList(values);
733735
}
734736

735-
/*
737+
/*
736738
* (non-Javadoc)
737739
* @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
738740
*/

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

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2015 the original author or authors.
2+
* Copyright 2013-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import static org.springframework.data.mongodb.core.DBObjectTestUtils.*;
2121
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
2222
import static org.springframework.data.mongodb.core.query.Criteria.*;
23+
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
2324

2425
import java.util.ArrayList;
2526
import java.util.List;
@@ -32,6 +33,7 @@
3233
import com.mongodb.BasicDBObject;
3334
import com.mongodb.BasicDBObjectBuilder;
3435
import com.mongodb.DBObject;
36+
import com.mongodb.util.JSON;
3537

3638
/**
3739
* Unit tests for {@link Aggregation}.
@@ -204,6 +206,47 @@ public void shouldSupportReferingToNestedPropertiesInGroupOperation() {
204206
assertThat(id.get("ruleType"), is((Object) "$rules.ruleType"));
205207
}
206208

209+
/**
210+
* @see DATAMONGO-1585
211+
*/
212+
@Test
213+
public void shouldSupportSortingBySyntheticAndExposedGroupFields() {
214+
215+
DBObject agg = newAggregation( //
216+
group("cmsParameterId").addToSet("title").as("titles"), //
217+
sort(Direction.ASC, "cmsParameterId", "titles") //
218+
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
219+
220+
assertThat(agg, is(notNullValue()));
221+
222+
DBObject sort = ((List<DBObject>) agg.get("pipeline")).get(1);
223+
224+
assertThat(getAsDBObject(sort, "$sort"), is(JSON.parse("{ \"_id.cmsParameterId\" : 1 , \"titles\" : 1}")));
225+
}
226+
227+
/**
228+
* @see DATAMONGO-1585
229+
*/
230+
@Test
231+
public void shouldSupportSortingByProjectedFields() {
232+
233+
DBObject agg = newAggregation( //
234+
project("cmsParameterId") //
235+
.and(SystemVariable.CURRENT + ".titles").as("titles") //
236+
.and("field").as("alias"), //
237+
sort(Direction.ASC, "cmsParameterId", "titles", "alias") //
238+
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
239+
240+
assertThat(agg, is(notNullValue()));
241+
242+
DBObject sort = ((List<DBObject>) agg.get("pipeline")).get(1);
243+
244+
assertThat(getAsDBObject(sort, "$sort"),
245+
isBsonObject().containing("cmsParameterId", 1) //
246+
.containing("titles", 1) //
247+
.containing("alias", 1));
248+
}
249+
207250
/**
208251
* @see DATAMONGO-924
209252
*/
@@ -248,19 +291,20 @@ public void shouldRenderAggregationWithCustomOptionsCorrectly() {
248291
DBObject agg = newAggregation( //
249292
project().and("a").as("aa") //
250293
) //
251-
.withOptions(aggregationOptions) //
294+
.withOptions(aggregationOptions) //
252295
.toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
253296

254-
assertThat(agg.toString(), is("{ \"aggregate\" : \"foo\" , " //
255-
+ "\"pipeline\" : [ { \"$project\" : { \"aa\" : \"$a\"}}] , " //
256-
+ "\"allowDiskUse\" : true , " //
257-
+ "\"explain\" : true , " //
258-
+ "\"cursor\" : { \"foo\" : 1}}" //
259-
));
297+
assertThat(agg.toString(),
298+
is("{ \"aggregate\" : \"foo\" , " //
299+
+ "\"pipeline\" : [ { \"$project\" : { \"aa\" : \"$a\"}}] , " //
300+
+ "\"allowDiskUse\" : true , " //
301+
+ "\"explain\" : true , " //
302+
+ "\"cursor\" : { \"foo\" : 1}}" //
303+
));
260304
}
261305

262306
/**
263-
* @see DATAMONGO-954
307+
* @see DATAMONGO-954, DATAMONGO-1585
264308
*/
265309
@Test
266310
public void shouldSupportReferencingSystemVariables() {
@@ -269,16 +313,16 @@ public void shouldSupportReferencingSystemVariables() {
269313
project("someKey") //
270314
.and("a").as("a1") //
271315
.and(Aggregation.CURRENT + ".a").as("a2") //
272-
, sort(Direction.DESC, "a") //
316+
, sort(Direction.DESC, "a1") //
273317
, group("someKey").first(Aggregation.ROOT).as("doc") //
274318
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
275319

276320
DBObject projection0 = extractPipelineElement(agg, 0, "$project");
277-
assertThat(projection0, is((DBObject) new BasicDBObject("someKey", 1).append("a1", "$a")
278-
.append("a2", "$$CURRENT.a")));
321+
assertThat(projection0,
322+
is((DBObject) new BasicDBObject("someKey", 1).append("a1", "$a").append("a2", "$$CURRENT.a")));
279323

280324
DBObject sort = extractPipelineElement(agg, 1, "$sort");
281-
assertThat(sort, is((DBObject) new BasicDBObject("a", -1)));
325+
assertThat(sort, is((DBObject) new BasicDBObject("a1", -1)));
282326

283327
DBObject group = extractPipelineElement(agg, 2, "$group");
284328
assertThat(group,
@@ -296,7 +340,7 @@ public void shouldExposeAliasedFieldnameForProjectionsIncludingOperationsDownThe
296340
.and("tags").minus(10).as("tags_count")//
297341
, group("date")//
298342
.sum("tags_count").as("count")//
299-
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
343+
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
300344

301345
DBObject group = extractPipelineElement(agg, 1, "$group");
302346
assertThat(getAsDBObject(group, "count"), is(new BasicDBObjectBuilder().add("$sum", "$tags_count").get()));
@@ -313,7 +357,7 @@ public void shouldUseAliasedFieldnameForProjectionsIncludingOperationsDownThePip
313357
.andExpression("tags-10")//
314358
, group("date")//
315359
.sum("tags_count").as("count")//
316-
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
360+
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
317361

318362
DBObject group = extractPipelineElement(agg, 1, "$group");
319363
assertThat(getAsDBObject(group, "count"), is(new BasicDBObjectBuilder().add("$sum", "$tags_count").get()));

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2016 the original author or authors.
2+
* Copyright 2013-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -171,6 +171,23 @@ public void rendersAggregationOptionsInTypedAggregationContextCorrectly() {
171171
assertThat(dbo.get("cursor"), is((Object) new BasicDBObject("foo", 1)));
172172
}
173173

174+
/**
175+
* @see DATAMONGO-1585
176+
*/
177+
@Test
178+
public void rendersSortOfProjectedFieldCorrectly() {
179+
180+
TypeBasedAggregationOperationContext context = getContext(MeterData.class);
181+
TypedAggregation<MeterData> agg = newAggregation(MeterData.class, project().and("counterName").as("counter"), //
182+
sort(Direction.ASC, "counter"));
183+
184+
DBObject dbo = agg.toDbObject("meterData", context);
185+
DBObject sort = getPipelineElementFromAggregationAt(dbo, 1);
186+
187+
DBObject definition = (DBObject) sort.get("$sort");
188+
assertThat(definition.get("counter"), is(equalTo((Object) 1)));
189+
}
190+
174191
/**
175192
* @see DATAMONGO-1133
176193
*/
@@ -190,21 +207,23 @@ public void shouldHonorAliasedFieldsInGroupExpressions() {
190207
}
191208

192209
/**
193-
* @see DATAMONGO-1326
210+
* @see DATAMONGO-1326, DATAMONGO-1585
194211
*/
195212
@Test
196213
public void lookupShouldInheritFieldsFromInheritingAggregationOperation() {
197214

198215
TypeBasedAggregationOperationContext context = getContext(MeterData.class);
199216
TypedAggregation<MeterData> agg = newAggregation(MeterData.class,
200-
lookup("OtherCollection", "resourceId", "otherId", "lookup"), sort(Direction.ASC, "resourceId"));
217+
lookup("OtherCollection", "resourceId", "otherId", "lookup"), //
218+
sort(Direction.ASC, "resourceId", "counterName"));
201219

202220
DBObject dbo = agg.toDbObject("meterData", context);
203221
DBObject sort = getPipelineElementFromAggregationAt(dbo, 1);
204222

205223
DBObject definition = (DBObject) sort.get("$sort");
206224

207225
assertThat(definition.get("resourceId"), is(equalTo((Object) 1)));
226+
assertThat(definition.get("counter_name"), is(equalTo((Object) 1)));
208227
}
209228

210229
/**

src/main/asciidoc/reference/mongodb.adoc

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1678,8 +1678,8 @@ Note that the aggregation operations not listed here are currently not supported
16781678
[[mongo.aggregation.projection]]
16791679
=== Projection Expressions
16801680

1681-
Projection expressions are used to define the fields that are the outcome of a particular aggregation step. Projection expressions can be defined via the `project` method of the `Aggregate` class either by passing a list of `String` 's or an aggregation framework `Fields` object. The projection can be extended with additional fields through a fluent API via the `and(String)` method and aliased via the `as(String)` method.
1682-
Note that one can also define fields with aliases via the static factory method `Fields.field` of the aggregation framework that can then be used to construct a new `Fields` instance.
1681+
Projection expressions are used to define the fields that are the outcome of a particular aggregation step. Projection expressions can be defined via the `project` method of the `Aggregate` class either by passing a list of ``String``'s or an aggregation framework `Fields` object. The projection can be extended with additional fields through a fluent API via the `and(String)` method and aliased via the `as(String)` method.
1682+
Note that one can also define fields with aliases via the static factory method `Fields.field` of the aggregation framework that can then be used to construct a new `Fields` instance. References to projected fields in later aggregation stages are only valid by using the field name of included fields or their alias of aliased or newly defined fields. Fields not included in the projection cannot be referenced in later aggregation stages.
16831683

16841684
.Projection expression examples
16851685
====
@@ -1691,9 +1691,19 @@ project("a","b").and("foo").as("bar") // will generate {$project: {a: 1, b: 1, b
16911691
----
16921692
====
16931693

1694-
Note that more examples for project operations can be found in the `AggregationTests` class.
1694+
.Multi-Stage Aggregation using Projection and Sorting
1695+
====
1696+
[source,java]
1697+
----
1698+
project("name", "netPrice"), sort(ASC, "name") // will generate {$project: {name: 1, netPrice: 1}}, {$sort: {name: 1}}
1699+
1700+
project().and("foo").as("bar"), sort(ASC, "bar") // will generate {$project: {bar: $foo}}, {$sort: {bar: 1}}
1701+
1702+
project().and("foo").as("bar"), sort(ASC, "foo") // this will not work
1703+
----
1704+
====
16951705

1696-
Note that further details regarding the projection expressions can be found in the http://docs.mongodb.org/manual/reference/operator/aggregation/project/#pipe._S_project[corresponding section] of the MongoDB Aggregation Framework reference documentation.
1706+
More examples for project operations can be found in the `AggregationTests` class. Note that further details regarding the projection expressions can be found in the http://docs.mongodb.org/manual/reference/operator/aggregation/project/#pipe._S_project[corresponding section] of the MongoDB Aggregation Framework reference documentation.
16971707

16981708
[[mongo.aggregation.projection.expressions]]
16991709
==== Spring Expression Support in Projection Expressions
@@ -1823,7 +1833,7 @@ ZipInfoStats firstZipInfoStats = result.getMappedResults().get(0);
18231833

18241834
* The class `ZipInfo` maps the structure of the given input-collection. The class `ZipInfoStats` defines the structure in the desired output format.
18251835
* As a first step we use the `group` operation to define a group from the input-collection. The grouping criteria is the combination of the fields `"state"` and `"city"` which forms the id structure of the group. We aggregate the value of the `"population"` property from the grouped elements with by using the `sum` operator saving the result in the field `"pop"`.
1826-
* In a second step we use the `sort` operation to sort the intermediate-result by the fields `"pop"`, `"state"` and `"city"` in ascending order, such that the smallest city is at the top and the biggest city is at the bottom of the result. Note that the sorting on "state" and `"city"` is implicitly performed against the group id fields which Spring Data MongoDB took care of.
1836+
* In a second step we use the `sort` operation to sort the intermediate-result by the fields `"pop"`, `"state"` and `"city"` in ascending order, such that the smallest city is at the top and the biggest city is at the bottom of the result. Note that the sorting on `"state"` and `"city"` is implicitly performed against the group id fields which Spring Data MongoDB took care of.
18271837
* In the third step we use a `group` operation again to group the intermediate result by `"state"`. Note that `"state"` again implicitly references an group-id field. We select the name and the population count of the biggest and smallest city with calls to the `last(…)` and `first(...)` operator respectively via the `project` operation.
18281838
* As the forth step we select the `"state"` field from the previous `group` operation. Note that `"state"` again implicitly references an group-id field. As we do not want an implicitly generated id to appear, we exclude the id from the previous operation via `and(previousOperation()).exclude()`. As we want to populate the nested `City` structures in our output-class accordingly we have to emit appropriate sub-documents with the nested method.
18291839
* Finally as the fifth step we sort the resulting list of `StateStats` by their state name in ascending order via the `sort` operation.

0 commit comments

Comments
 (0)