Skip to content

Commit 361f9da

Browse files
Thomas Darimontodrotbohm
Thomas Darimont
authored andcommitted
DATAMONGO-753 - Add support for nested field references in aggregation operations.
Aggregation pipelines now correctly handle nested field references in aggregation operations. We introduced FieldsExposingAggregationOperation to mark AggregationOperations that change the set of exposed fields available for processing by later AggregationOperations. Extracted context state out of AggregationOperation to ExposedFieldsAggregationContext for better separation of concerns. Modified toDbObject(…) in Aggregation to only replace the aggregation context when the current AggregationOperation is a FieldExposingAggregationOperation. Original pull request: #74.
1 parent 56b23a6 commit 361f9da

File tree

9 files changed

+191
-30
lines changed

9 files changed

+191
-30
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,9 @@ public DBObject toDbObject(String inputCollectionName, AggregationOperationConte
227227

228228
operationDocuments.add(operation.toDBObject(context));
229229

230-
if (operation instanceof AggregationOperationContext) {
231-
context = (AggregationOperationContext) operation;
230+
if (operation instanceof FieldsExposingAggregationOperation) {
231+
FieldsExposingAggregationOperation exposedFieldsOperation = (FieldsExposingAggregationOperation) operation;
232+
context = new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields());
232233
}
233234
}
234235

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,32 @@
1717

1818
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
1919
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
20+
import org.springframework.util.Assert;
2021

2122
import com.mongodb.DBObject;
2223

2324
/**
24-
* Support class to implement {@link AggregationOperation}s that will become an {@link AggregationOperationContext} as
25-
* well defining {@link ExposedFields}.
25+
* {@link AggregationOperationContext} that combines the available field references from a given
26+
* {@code AggregationOperationContext} and an {@link FieldsExposingAggregationOperation}.
2627
*
28+
* @author Thomas Darimont
2729
* @author Oliver Gierke
28-
* @since 1.3
30+
* @since 1.4
2931
*/
30-
public abstract class ExposedFieldsAggregationOperationContext implements AggregationOperationContext {
32+
class ExposedFieldsAggregationOperationContext implements AggregationOperationContext {
33+
34+
private final ExposedFields exposedFields;
35+
36+
/**
37+
* Creates a new {@link ExposedFieldsAggregationOperationContext} from the given {@link ExposedFields}.
38+
*
39+
* @param exposedFields must not be {@literal null}.
40+
*/
41+
public ExposedFieldsAggregationOperationContext(ExposedFields exposedFields) {
42+
43+
Assert.notNull(exposedFields, "ExposedFields must not be null!");
44+
this.exposedFields = exposedFields;
45+
}
3146

3247
/*
3348
* (non-Javadoc)
@@ -54,14 +69,12 @@ public FieldReference getReference(Field field) {
5469
@Override
5570
public FieldReference getReference(String name) {
5671

57-
ExposedField field = getFields().getField(name);
72+
ExposedField field = exposedFields.getField(name);
5873

5974
if (field != null) {
6075
return new FieldReference(field);
6176
}
6277

6378
throw new IllegalArgumentException(String.format("Invalid reference '%s'!", name));
6479
}
65-
66-
protected abstract ExposedFields getFields();
6780
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2013 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+
/**
19+
* {@link AggregationOperation} that exposes new {@link ExposedFields} that can be used for later aggregation pipeline
20+
* {@code AggregationOperation}s.
21+
*
22+
* @author Thomas Darimont
23+
*/
24+
public interface FieldsExposingAggregationOperation extends AggregationOperation {
25+
26+
/**
27+
* Returns the fields exposed by the {@link AggregationOperation}.
28+
*
29+
* @return will never be {@literal null}.
30+
*/
31+
ExposedFields getFields();
32+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
* @author Oliver Gierke
3939
* @since 1.3
4040
*/
41-
public class GroupOperation extends ExposedFieldsAggregationOperationContext implements AggregationOperation {
41+
public class GroupOperation implements FieldsExposingAggregationOperation {
4242

4343
private final ExposedFields nonSynthecticFields;
4444
private final List<Operation> operations;

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
* @author Oliver Gierke
4242
* @since 1.3
4343
*/
44-
public class ProjectionOperation extends ExposedFieldsAggregationOperationContext implements AggregationOperation {
44+
public class ProjectionOperation implements FieldsExposingAggregationOperation {
4545

4646
private static final List<Projection> NONE = Collections.emptyList();
4747

@@ -149,10 +149,10 @@ public ProjectionOperation andInclude(Fields fields) {
149149

150150
/*
151151
* (non-Javadoc)
152-
* @see org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext#getFields()
152+
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContextSupport#getFields()
153153
*/
154154
@Override
155-
protected ExposedFields getFields() {
155+
public ExposedFields getFields() {
156156

157157
ExposedFields fields = null;
158158

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ public DBObject getMappedObject(DBObject dbObject) {
7171
return mapper.getMappedObject(dbObject, mappingContext.getPersistentEntity(type));
7272
}
7373

74-
/*
74+
/*
7575
* (non-Javadoc)
76-
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(org.springframework.data.mongodb.core.aggregation.ExposedFields.AvailableField)
76+
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(org.springframework.data.mongodb.core.aggregation.Field)
7777
*/
7878
@Override
7979
public FieldReference getReference(Field field) {

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

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
* @author Oliver Gierke
3030
* @since 1.3
3131
*/
32-
public class UnwindOperation extends ExposedFieldsAggregationOperationContext implements AggregationOperation {
32+
public class UnwindOperation implements AggregationOperation {
3333

3434
private final ExposedField field;
3535

@@ -44,15 +44,6 @@ public UnwindOperation(Field field) {
4444
this.field = new ExposedField(field, true);
4545
}
4646

47-
/*
48-
* (non-Javadoc)
49-
* @see org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext#getFields()
50-
*/
51-
@Override
52-
protected ExposedFields getFields() {
53-
return ExposedFields.from(field);
54-
}
55-
5647
/*
5748
* (non-Javadoc)
5849
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)

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

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.core.aggregation;
1717

18-
import static org.hamcrest.CoreMatchers.*;
18+
import static org.hamcrest.Matchers.*;
1919
import static org.junit.Assert.*;
2020
import static org.springframework.data.domain.Sort.Direction.*;
2121
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
@@ -84,6 +84,7 @@ private void cleanDb() {
8484
mongoTemplate.dropCollection(INPUT_COLLECTION);
8585
mongoTemplate.dropCollection(Product.class);
8686
mongoTemplate.dropCollection(UserWithLikes.class);
87+
mongoTemplate.dropCollection(DATAMONGO753.class);
8788
}
8889

8990
/**
@@ -452,7 +453,61 @@ public void arithmenticOperatorsInProjectionExample() {
452453
assertThat((Double) resultList.get(0).get("netPriceMul2"), is(netPrice * 2));
453454
assertThat((Double) resultList.get(0).get("netPriceDiv119"), is(netPrice / 1.19));
454455
assertThat((Integer) resultList.get(0).get("spaceUnitsMod2"), is(spaceUnits % 2));
456+
}
457+
458+
/**
459+
* @see DATAMONGO-753
460+
* @see http
461+
* ://stackoverflow.com/questions/18653574/spring-data-mongodb-aggregation-framework-invalid-reference-in-group
462+
* -operati
463+
*/
464+
@Test
465+
public void allowsNestedFieldReferencesAsGroupIdsInGroupExpressions() {
466+
467+
mongoTemplate.insert(new DATAMONGO753().withPDs(new PD("A", 1), new PD("B", 1), new PD("C", 1)));
468+
mongoTemplate.insert(new DATAMONGO753().withPDs(new PD("B", 1), new PD("B", 1), new PD("C", 1)));
469+
470+
TypedAggregation<DATAMONGO753> agg = newAggregation(DATAMONGO753.class, //
471+
unwind("pd"), //
472+
group("pd.pDch") // the nested field expression
473+
.sum("pd.up").as("uplift"), //
474+
project("_id", "uplift"));
455475

476+
AggregationResults<DBObject> result = mongoTemplate.aggregate(agg, DBObject.class);
477+
List<DBObject> stats = result.getMappedResults();
478+
479+
assertThat(stats.size(), is(3));
480+
assertThat(stats.get(0).get("_id").toString(), is("C"));
481+
assertThat((Integer) stats.get(0).get("uplift"), is(2));
482+
assertThat(stats.get(1).get("_id").toString(), is("B"));
483+
assertThat((Integer) stats.get(1).get("uplift"), is(3));
484+
assertThat(stats.get(2).get("_id").toString(), is("A"));
485+
assertThat((Integer) stats.get(2).get("uplift"), is(1));
486+
}
487+
488+
/**
489+
* @see DATAMONGO-753
490+
* @see http
491+
* ://stackoverflow.com/questions/18653574/spring-data-mongodb-aggregation-framework-invalid-reference-in-group
492+
* -operati
493+
*/
494+
@Test
495+
public void aliasesNestedFieldInProjectionImmediately() {
496+
497+
mongoTemplate.insert(new DATAMONGO753().withPDs(new PD("A", 1), new PD("B", 1), new PD("C", 1)));
498+
mongoTemplate.insert(new DATAMONGO753().withPDs(new PD("B", 1), new PD("B", 1), new PD("C", 1)));
499+
500+
TypedAggregation<DATAMONGO753> agg = newAggregation(DATAMONGO753.class, //
501+
unwind("pd"), //
502+
project().and("pd.up").as("up"));
503+
504+
AggregationResults<DBObject> results = mongoTemplate.aggregate(agg, DBObject.class);
505+
List<DBObject> mappedResults = results.getMappedResults();
506+
507+
assertThat(mappedResults, hasSize(6));
508+
for (DBObject element : mappedResults) {
509+
assertThat(element.get("up"), is((Object) 1));
510+
}
456511
}
457512

458513
private void assertLikeStats(LikeStats like, String id, long count) {
@@ -502,4 +557,22 @@ private static void assertTagCount(String tag, int n, TagCount tagCount) {
502557
assertThat(tagCount.getN(), is(n));
503558
}
504559

560+
static class DATAMONGO753 {
561+
PD[] pd;
562+
563+
DATAMONGO753 withPDs(PD... pds) {
564+
this.pd = pds;
565+
return this;
566+
}
567+
}
568+
569+
static class PD {
570+
String pDch;
571+
@org.springframework.data.mongodb.core.mapping.Field("alias") int up;
572+
573+
public PD(String pDch, int up) {
574+
this.pDch = pDch;
575+
this.up = up;
576+
}
577+
}
505578
}

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

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,83 @@
1515
*/
1616
package org.springframework.data.mongodb.core.aggregation;
1717

18+
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
19+
import static org.springframework.data.mongodb.core.query.Criteria.*;
20+
21+
import org.junit.Rule;
1822
import org.junit.Test;
23+
import org.junit.rules.ExpectedException;
1924

2025
/**
2126
* Unit tests for {@link Aggregation}.
2227
*
2328
* @author Oliver Gierke
29+
* @author Thomas Darimont
2430
*/
2531
public class AggregationUnitTests {
2632

33+
public @Rule ExpectedException exception = ExpectedException.none();
34+
2735
@Test(expected = IllegalArgumentException.class)
2836
public void rejectsNullAggregationOperation() {
29-
Aggregation.newAggregation((AggregationOperation[]) null);
37+
newAggregation((AggregationOperation[]) null);
3038
}
3139

3240
@Test(expected = IllegalArgumentException.class)
3341
public void rejectsNullTypedAggregationOperation() {
34-
Aggregation.newAggregation(String.class, (AggregationOperation[]) null);
42+
newAggregation(String.class, (AggregationOperation[]) null);
3543
}
3644

3745
@Test(expected = IllegalArgumentException.class)
3846
public void rejectsNoAggregationOperation() {
39-
Aggregation.newAggregation(new AggregationOperation[0]);
47+
newAggregation(new AggregationOperation[0]);
4048
}
4149

4250
@Test(expected = IllegalArgumentException.class)
4351
public void rejectsNoTypedAggregationOperation() {
44-
Aggregation.newAggregation(String.class, new AggregationOperation[0]);
52+
newAggregation(String.class, new AggregationOperation[0]);
53+
}
54+
55+
/**
56+
* @see DATAMONGO-753
57+
*/
58+
@Test
59+
public void checkForCorrectFieldScopeTransfer() {
60+
61+
exception.expect(IllegalArgumentException.class);
62+
exception.expectMessage("Invalid reference");
63+
exception.expectMessage("'b'");
64+
65+
newAggregation( //
66+
project("a", "b"), //
67+
group("a").count().as("cnt"), // a was introduced to the context by the project operation
68+
project("cnt", "b") // b was removed from the context by the group operation
69+
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT); // -> triggers IllegalArgumentException
70+
}
71+
72+
/**
73+
* @see DATAMONGO-753
74+
*/
75+
@Test
76+
public void unwindOperationShouldNotChangeAvailableFields() {
77+
78+
newAggregation( //
79+
project("a", "b"), //
80+
unwind("a"), //
81+
project("a", "b") // b should still be available
82+
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
83+
}
84+
85+
/**
86+
* @see DATAMONGO-753
87+
*/
88+
@Test
89+
public void matchOperationShouldNotChangeAvailableFields() {
90+
91+
newAggregation( //
92+
project("a", "b"), //
93+
match(where("a").gte(1)), //
94+
project("a", "b") // b should still be available
95+
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
4596
}
4697
}

0 commit comments

Comments
 (0)