Skip to content

Commit c3e5fca

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 8340c02 commit c3e5fca

File tree

4 files changed

+93
-19
lines changed

4 files changed

+93
-19
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-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.
@@ -21,12 +21,12 @@
2121
import java.util.Collections;
2222
import java.util.List;
2323

24-
import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable;
2524
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond;
2625
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.IfNull;
2726
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
2827
import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField;
2928
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.FieldProjection;
29+
import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable;
3030
import org.springframework.util.Assert;
3131

3232
import com.mongodb.BasicDBObject;
@@ -1208,8 +1208,9 @@ public ProjectionOperationBuilder dateAsFormattedString(String format) {
12081208
* @since 1.10
12091209
*/
12101210
public ProjectionOperationBuilder let(AggregationExpression valueExpression, String variableName,
1211-
AggregationExpression in) {
1212-
return this.operation.and(VariableOperators.Let.define(ExpressionVariable.newVariable(variableName).forExpression(valueExpression)).andApply(in));
1211+
AggregationExpression in) {
1212+
return this.operation.and(VariableOperators.Let
1213+
.define(ExpressionVariable.newVariable(variableName).forExpression(valueExpression)).andApply(in));
12131214
}
12141215

12151216
/**
@@ -1281,6 +1282,7 @@ public DBObject toDBObject(AggregationOperationContext context) {
12811282
*
12821283
* @author Oliver Gierke
12831284
* @author Thomas Darimont
1285+
* @author Mark Paluch
12841286
*/
12851287
static class FieldProjection extends Projection {
12861288

@@ -1299,7 +1301,7 @@ public FieldProjection(String name, Object value) {
12991301

13001302
private FieldProjection(Field field, Object value) {
13011303

1302-
super(field);
1304+
super(new ExposedField(field.getName(), true));
13031305

13041306
this.field = field;
13051307
this.value = value;

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

Lines changed: 47 additions & 5 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.
@@ -31,11 +31,12 @@
3131
import org.junit.rules.ExpectedException;
3232
import org.springframework.data.domain.Sort.Direction;
3333
import org.springframework.data.mongodb.core.query.Criteria;
34+
import org.springframework.data.mongodb.test.util.BasicDbListBuilder;
3435

3536
import com.mongodb.BasicDBObject;
3637
import com.mongodb.BasicDBObjectBuilder;
3738
import com.mongodb.DBObject;
38-
import org.springframework.data.mongodb.test.util.BasicDbListBuilder;
39+
import com.mongodb.util.JSON;
3940

4041
/**
4142
* Unit tests for {@link Aggregation}.
@@ -282,6 +283,47 @@ public void shouldSupportReferingToNestedPropertiesInGroupOperation() {
282283
assertThat(id.get("ruleType"), is((Object) "$rules.ruleType"));
283284
}
284285

286+
/**
287+
* @see DATAMONGO-1585
288+
*/
289+
@Test
290+
public void shouldSupportSortingBySyntheticAndExposedGroupFields() {
291+
292+
DBObject agg = newAggregation( //
293+
group("cmsParameterId").addToSet("title").as("titles"), //
294+
sort(Direction.ASC, "cmsParameterId", "titles") //
295+
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
296+
297+
assertThat(agg, is(notNullValue()));
298+
299+
DBObject sort = ((List<DBObject>) agg.get("pipeline")).get(1);
300+
301+
assertThat(getAsDBObject(sort, "$sort"), is(JSON.parse("{ \"_id.cmsParameterId\" : 1 , \"titles\" : 1}")));
302+
}
303+
304+
/**
305+
* @see DATAMONGO-1585
306+
*/
307+
@Test
308+
public void shouldSupportSortingByProjectedFields() {
309+
310+
DBObject agg = newAggregation( //
311+
project("cmsParameterId") //
312+
.and(SystemVariable.CURRENT + ".titles").as("titles") //
313+
.and("field").as("alias"), //
314+
sort(Direction.ASC, "cmsParameterId", "titles", "alias") //
315+
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
316+
317+
assertThat(agg, is(notNullValue()));
318+
319+
DBObject sort = ((List<DBObject>) agg.get("pipeline")).get(1);
320+
321+
assertThat(getAsDBObject(sort, "$sort"),
322+
isBsonObject().containing("cmsParameterId", 1) //
323+
.containing("titles", 1) //
324+
.containing("alias", 1));
325+
}
326+
285327
/**
286328
* @see DATAMONGO-924
287329
*/
@@ -339,7 +381,7 @@ public void shouldRenderAggregationWithCustomOptionsCorrectly() {
339381
}
340382

341383
/**
342-
* @see DATAMONGO-954
384+
* @see DATAMONGO-954, DATAMONGO-1585
343385
*/
344386
@Test
345387
public void shouldSupportReferencingSystemVariables() {
@@ -348,7 +390,7 @@ public void shouldSupportReferencingSystemVariables() {
348390
project("someKey") //
349391
.and("a").as("a1") //
350392
.and(Aggregation.CURRENT + ".a").as("a2") //
351-
, sort(Direction.DESC, "a") //
393+
, sort(Direction.DESC, "a1") //
352394
, group("someKey").first(Aggregation.ROOT).as("doc") //
353395
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
354396

@@ -357,7 +399,7 @@ public void shouldSupportReferencingSystemVariables() {
357399
is((DBObject) new BasicDBObject("someKey", 1).append("a1", "$a").append("a2", "$$CURRENT.a")));
358400

359401
DBObject sort = extractPipelineElement(agg, 1, "$sort");
360-
assertThat(sort, is((DBObject) new BasicDBObject("a", -1)));
402+
assertThat(sort, is((DBObject) new BasicDBObject("a1", -1)));
361403

362404
DBObject group = extractPipelineElement(agg, 2, "$group");
363405
assertThat(group,

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

Lines changed: 24 additions & 4 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.
@@ -107,7 +107,8 @@ public void returnsReferencesToNestedFieldsCorrectly() {
107107
public void aliasesIdFieldCorrectly() {
108108

109109
AggregationOperationContext context = getContext(Foo.class);
110-
assertThat(context.getReference("id"), is((FieldReference) new DirectFieldReference(new ExposedField(field("id", "_id"), true))));
110+
assertThat(context.getReference("id"),
111+
is((FieldReference) new DirectFieldReference(new ExposedField(field("id", "_id"), true))));
111112
}
112113

113114
/**
@@ -175,6 +176,23 @@ public void rendersAggregationOptionsInTypedAggregationContextCorrectly() {
175176
assertThat(dbo.get("cursor"), is((Object) new BasicDBObject("foo", 1)));
176177
}
177178

179+
/**
180+
* @see DATAMONGO-1585
181+
*/
182+
@Test
183+
public void rendersSortOfProjectedFieldCorrectly() {
184+
185+
TypeBasedAggregationOperationContext context = getContext(MeterData.class);
186+
TypedAggregation<MeterData> agg = newAggregation(MeterData.class, project().and("counterName").as("counter"), //
187+
sort(Direction.ASC, "counter"));
188+
189+
DBObject dbo = agg.toDbObject("meterData", context);
190+
DBObject sort = getPipelineElementFromAggregationAt(dbo, 1);
191+
192+
DBObject definition = (DBObject) sort.get("$sort");
193+
assertThat(definition.get("counter"), is(equalTo((Object) 1)));
194+
}
195+
178196
/**
179197
* @see DATAMONGO-1133
180198
*/
@@ -194,21 +212,23 @@ public void shouldHonorAliasedFieldsInGroupExpressions() {
194212
}
195213

196214
/**
197-
* @see DATAMONGO-1326
215+
* @see DATAMONGO-1326, DATAMONGO-1585
198216
*/
199217
@Test
200218
public void lookupShouldInheritFieldsFromInheritingAggregationOperation() {
201219

202220
TypeBasedAggregationOperationContext context = getContext(MeterData.class);
203221
TypedAggregation<MeterData> agg = newAggregation(MeterData.class,
204-
lookup("OtherCollection", "resourceId", "otherId", "lookup"), sort(Direction.ASC, "resourceId"));
222+
lookup("OtherCollection", "resourceId", "otherId", "lookup"), //
223+
sort(Direction.ASC, "resourceId", "counterName"));
205224

206225
DBObject dbo = agg.toDbObject("meterData", context);
207226
DBObject sort = getPipelineElementFromAggregationAt(dbo, 1);
208227

209228
DBObject definition = (DBObject) sort.get("$sort");
210229

211230
assertThat(definition.get("resourceId"), is(equalTo((Object) 1)));
231+
assertThat(definition.get("counter_name"), is(equalTo((Object) 1)));
212232
}
213233

214234
/**

src/main/asciidoc/reference/mongodb.adoc

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

1735-
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.
1736-
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.
1735+
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.
1736+
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.
17371737

17381738
.Projection expression examples
17391739
====
@@ -1745,9 +1745,19 @@ project("a","b").and("foo").as("bar") // will generate {$project: {a: 1, b: 1, b
17451745
----
17461746
====
17471747

1748-
Note that more examples for project operations can be found in the `AggregationTests` class.
1748+
.Multi-Stage Aggregation using Projection and Sorting
1749+
====
1750+
[source,java]
1751+
----
1752+
project("name", "netPrice"), sort(ASC, "name") // will generate {$project: {name: 1, netPrice: 1}}, {$sort: {name: 1}}
1753+
1754+
project().and("foo").as("bar"), sort(ASC, "bar") // will generate {$project: {bar: $foo}}, {$sort: {bar: 1}}
1755+
1756+
project().and("foo").as("bar"), sort(ASC, "foo") // this will not work
1757+
----
1758+
====
17491759

1750-
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.
1760+
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.
17511761

17521762
[[mongo.aggregation.facet]]
17531763
=== Faceted classification
@@ -1998,7 +2008,7 @@ ZipInfoStats firstZipInfoStats = result.getMappedResults().get(0);
19982008

19992009
* The class `ZipInfo` maps the structure of the given input-collection. The class `ZipInfoStats` defines the structure in the desired output format.
20002010
* 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"`.
2001-
* 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.
2011+
* 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.
20022012
* 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.
20032013
* 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.
20042014
* 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)