Skip to content

Commit 9a078b7

Browse files
mp911dechristophstrobl
authored andcommitted
DATAMONGO-1326 - Support field inheritance for $lookup aggregation operator.
We now distinguish between aggregation operations that replace fields in the aggregation pipeline and those which inherit fields from previous operations. InheritsFieldsAggregationOperation is a nested interface of FieldsExposingAggregationOperation is a marker to lookup fields along the aggregation context chain. Added unit and integration tests. Mention lookup operator in docs. Original pull request: #344.
1 parent 65b6576 commit 9a078b7

File tree

9 files changed

+353
-21
lines changed

9 files changed

+353
-21
lines changed

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

Lines changed: 9 additions & 2 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-2016 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.
@@ -26,6 +26,7 @@
2626
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
2727
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
2828
import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField;
29+
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
2930
import org.springframework.data.mongodb.core.query.Criteria;
3031
import org.springframework.data.mongodb.core.query.NearQuery;
3132
import org.springframework.data.mongodb.core.query.SerializationUtils;
@@ -41,6 +42,7 @@
4142
* @author Tobias Trelle
4243
* @author Thomas Darimont
4344
* @author Oliver Gierke
45+
* @author Mark Paluch
4446
* @author Alessio Fachechi
4547
* @since 1.3
4648
*/
@@ -362,7 +364,12 @@ public DBObject toDbObject(String inputCollectionName, AggregationOperationConte
362364
if (operation instanceof FieldsExposingAggregationOperation) {
363365

364366
FieldsExposingAggregationOperation exposedFieldsOperation = (FieldsExposingAggregationOperation) operation;
365-
context = new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), rootContext);
367+
368+
if (operation instanceof InheritsFieldsAggregationOperation) {
369+
context = new InheritingExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), context);
370+
} else {
371+
context = new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), context);
372+
}
366373
}
367374
}
368375

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2014 the original author or authors.
2+
* Copyright 2013-2016 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.
@@ -27,6 +27,7 @@
2727
*
2828
* @author Thomas Darimont
2929
* @author Oliver Gierke
30+
* @author Mark Paluch
3031
* @since 1.4
3132
*/
3233
class ExposedFieldsAggregationOperationContext implements AggregationOperationContext {
@@ -89,6 +90,22 @@ private FieldReference getReference(Field field, String name) {
8990

9091
Assert.notNull(name, "Name must not be null!");
9192

93+
FieldReference exposedField = resolveExposedField(field, name);
94+
if (exposedField != null) {
95+
return exposedField;
96+
}
97+
98+
throw new IllegalArgumentException(String.format("Invalid reference '%s'!", name));
99+
}
100+
101+
/**
102+
* Resolves a {@link field}/{@link name} for a {@link FieldReference} if possible.
103+
*
104+
* @param field may be {@literal null}
105+
* @param name must not be {@literal null}
106+
* @return the resolved reference or {@literal null}
107+
*/
108+
protected FieldReference resolveExposedField(Field field, String name) {
92109
ExposedField exposedField = exposedFields.getField(name);
93110

94111
if (exposedField != null) {
@@ -112,7 +129,6 @@ private FieldReference getReference(Field field, String name) {
112129
return new FieldReference(new ExposedField(name, true));
113130
}
114131
}
115-
116-
throw new IllegalArgumentException(String.format("Invalid reference '%s'!", name));
132+
return null;
117133
}
118134
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013 the original author or authors.
2+
* Copyright 2013-2016 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.
@@ -16,17 +16,28 @@
1616
package org.springframework.data.mongodb.core.aggregation;
1717

1818
/**
19-
* {@link AggregationOperation} that exposes new {@link ExposedFields} that can be used for later aggregation pipeline
20-
* {@code AggregationOperation}s.
21-
*
19+
* {@link AggregationOperation} that exposes {@link ExposedFields} that can be used for later aggregation pipeline
20+
* {@code AggregationOperation}s. A {@link FieldsExposingAggregationOperation} implementing the
21+
* {@link InheritsFieldsAggregationOperation} will expose fields from its parent operations. Not implementing
22+
* {@link InheritsFieldsAggregationOperation} will replace existing exposed fields.
23+
*
2224
* @author Thomas Darimont
25+
* @author Mark Paluch
2326
*/
2427
public interface FieldsExposingAggregationOperation extends AggregationOperation {
2528

2629
/**
2730
* Returns the fields exposed by the {@link AggregationOperation}.
28-
*
31+
*
2932
* @return will never be {@literal null}.
3033
*/
3134
ExposedFields getFields();
35+
36+
/**
37+
* Marker interface for {@link AggregationOperation} that inherits fields from previous operations.
38+
*/
39+
static interface InheritsFieldsAggregationOperation extends FieldsExposingAggregationOperation {
40+
41+
}
42+
3243
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
17+
package org.springframework.data.mongodb.core.aggregation;
18+
19+
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
20+
import org.springframework.util.Assert;
21+
22+
/**
23+
* {@link ExposedFieldsAggregationOperationContext} that inherits fields from its parent
24+
* {@link AggregationOperationContext}.
25+
*
26+
* @author Mark Paluch
27+
*/
28+
class InheritingExposedFieldsAggregationOperationContext extends ExposedFieldsAggregationOperationContext {
29+
30+
private final AggregationOperationContext previousContext;
31+
32+
/**
33+
* Creates a new {@link ExposedFieldsAggregationOperationContext} from the given {@link ExposedFields}. Uses the given
34+
* {@link AggregationOperationContext} to perform a mapping to mongo types if necessary.
35+
*
36+
* @param exposedFields must not be {@literal null}.
37+
* @param previousContext must not be {@literal null}.
38+
*/
39+
public InheritingExposedFieldsAggregationOperationContext(ExposedFields exposedFields,
40+
AggregationOperationContext previousContext) {
41+
42+
super(exposedFields, previousContext);
43+
Assert.notNull(previousContext, "PreviousContext must not be null!");
44+
this.previousContext = previousContext;
45+
}
46+
47+
/*
48+
* (non-Javadoc)
49+
* @see org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext#resolveExposedField(org.springframework.data.mongodb.core.aggregation.Field, java.lang.String)
50+
*/
51+
@Override
52+
protected FieldReference resolveExposedField(Field field, String name) {
53+
54+
FieldReference fieldReference = super.resolveExposedField(field, name);
55+
if (fieldReference != null) {
56+
return fieldReference;
57+
}
58+
59+
if (field != null) {
60+
return previousContext.getReference(field);
61+
}
62+
63+
return previousContext.getReference(name);
64+
}
65+
}

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.mongodb.core.aggregation;
1717

1818
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
19+
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
1920
import org.springframework.util.Assert;
2021

2122
import com.mongodb.BasicDBObject;
@@ -27,10 +28,11 @@
2728
*
2829
* @author Alessio Fachechi
2930
* @author Christoph Strobl
31+
* @author Mark Paluch
3032
* @see http://docs.mongodb.org/manual/reference/aggregation/lookup/#stage._S_lookup
3133
* @since 1.9
3234
*/
33-
public class LookupOperation implements FieldsExposingAggregationOperation {
35+
public class LookupOperation implements FieldsExposingAggregationOperation, InheritsFieldsAggregationOperation {
3436

3537
private Field from;
3638
private Field localField;
@@ -100,7 +102,7 @@ public static FromBuilder newLookup() {
100102
public static interface FromBuilder {
101103

102104
/**
103-
* @param name
105+
* @param name the collection in the same database to perform the join with, must not be {@literal null} or empty.
104106
* @return
105107
*/
106108
LocalFieldBuilder from(String name);
@@ -109,7 +111,8 @@ public static interface FromBuilder {
109111
public static interface LocalFieldBuilder {
110112

111113
/**
112-
* @param name
114+
* @param name the field from the documents input to the {@code $lookup} stage, must not be {@literal null} or
115+
* empty.
113116
* @return
114117
*/
115118
ForeignFieldBuilder localField(String name);
@@ -118,7 +121,7 @@ public static interface LocalFieldBuilder {
118121
public static interface ForeignFieldBuilder {
119122

120123
/**
121-
* @param name
124+
* @param name the field from the documents in the {@code from} collection, must not be {@literal null} or empty.
122125
* @return
123126
*/
124127
AsBuilder foreignField(String name);
@@ -127,30 +130,30 @@ public static interface ForeignFieldBuilder {
127130
public static interface AsBuilder {
128131

129132
/**
130-
* @param name
133+
* @param name the name of the new array field to add to the input documents, must not be {@literal null} or empty.
131134
* @return
132135
*/
133136
LookupOperation as(String name);
134137
}
135138

136139
/**
137140
* Builder for fluent {@link LookupOperation} creation.
138-
*
141+
*
139142
* @author Christoph Strobl
140143
* @since 1.9
141144
*/
142145
public static final class LookupOperationBuilder
143146
implements FromBuilder, LocalFieldBuilder, ForeignFieldBuilder, AsBuilder {
144147

145-
private LookupOperation lookupOperation;
148+
private final LookupOperation lookupOperation;
146149

147150
private LookupOperationBuilder() {
148151
this.lookupOperation = new LookupOperation();
149152
}
150153

151154
/**
152155
* Creates new builder for {@link LookupOperation}.
153-
*
156+
*
154157
* @return never {@literal null}.
155158
*/
156159
public static FromBuilder newBuilder() {
@@ -170,7 +173,8 @@ public LookupOperation as(String name) {
170173

171174
Assert.hasText(name, "'As' must not be null or empty!");
172175
lookupOperation.as = new ExposedField(Fields.field(name), true);
173-
return null;
176+
return new LookupOperation(lookupOperation.from, lookupOperation.localField, lookupOperation.foreignField,
177+
lookupOperation.as);
174178
}
175179

176180
@Override

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
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2015 the original author or authors.
2+
* Copyright 2013-2016 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,6 +21,7 @@
2121
import static org.springframework.data.domain.Sort.Direction.*;
2222
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
2323
import static org.springframework.data.mongodb.core.query.Criteria.*;
24+
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
2425

2526
import java.io.BufferedInputStream;
2627
import java.text.ParseException;
@@ -76,6 +77,7 @@
7677
* @author Thomas Darimont
7778
* @author Oliver Gierke
7879
* @author Christoph Strobl
80+
* @author Mark Paluch
7981
*/
8082
@RunWith(SpringJUnit4ClassRunner.class)
8183
@ContextConfiguration("classpath:infrastructure.xml")
@@ -85,6 +87,7 @@ public class AggregationTests {
8587
private static final Logger LOGGER = LoggerFactory.getLogger(AggregationTests.class);
8688
private static final Version TWO_DOT_FOUR = new Version(2, 4);
8789
private static final Version TWO_DOT_SIX = new Version(2, 6);
90+
private static final Version THREE_DOT_TWO = new Version(3, 2);
8891

8992
private static boolean initialized = false;
9093

@@ -1068,6 +1071,76 @@ public void shouldHonorFieldAliasesForFieldReferences() {
10681071
assertThat(result.get("totalValue"), is(equalTo((Object) 100.0)));
10691072
}
10701073

1074+
/**
1075+
* @see DATAMONGO-1326
1076+
*/
1077+
@Test
1078+
public void shouldLookupPeopleCorectly() {
1079+
1080+
assumeTrue(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO));
1081+
1082+
createUsersWithReferencedPersons();
1083+
1084+
TypedAggregation<User> agg = newAggregation(User.class, //
1085+
lookup("person", "_id", "firstname", "linkedPerson"), //
1086+
sort(ASC, "id"));
1087+
1088+
AggregationResults<DBObject> results = mongoTemplate.aggregate(agg, User.class, DBObject.class);
1089+
1090+
List<DBObject> mappedResults = results.getMappedResults();
1091+
1092+
DBObject firstItem = mappedResults.get(0);
1093+
1094+
assertThat(firstItem, isBsonObject().containing("_id", "u1"));
1095+
assertThat(firstItem, isBsonObject().containing("linkedPerson.[0].firstname", "u1"));
1096+
}
1097+
1098+
/**
1099+
* @see DATAMONGO-1326
1100+
*/
1101+
@Test
1102+
public void shouldGroupByAndLookupPeopleCorectly() {
1103+
1104+
assumeTrue(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO));
1105+
1106+
createUsersWithReferencedPersons();
1107+
1108+
TypedAggregation<User> agg = newAggregation(User.class, //
1109+
group().min("id").as("foreignKey"), //
1110+
lookup("person", "foreignKey", "firstname", "linkedPerson"), //
1111+
sort(ASC, "foreignKey", "linkedPerson.firstname"));
1112+
1113+
AggregationResults<DBObject> results = mongoTemplate.aggregate(agg, User.class, DBObject.class);
1114+
1115+
List<DBObject> mappedResults = results.getMappedResults();
1116+
1117+
DBObject firstItem = mappedResults.get(0);
1118+
1119+
assertThat(firstItem, isBsonObject().containing("foreignKey", "u1"));
1120+
assertThat(firstItem, isBsonObject().containing("linkedPerson.[0].firstname", "u1"));
1121+
}
1122+
1123+
private void createUsersWithReferencedPersons() {
1124+
1125+
mongoTemplate.dropCollection(User.class);
1126+
mongoTemplate.dropCollection(Person.class);
1127+
1128+
User user1 = new User("u1");
1129+
User user2 = new User("u2");
1130+
User user3 = new User("u3");
1131+
1132+
mongoTemplate.save(user1);
1133+
mongoTemplate.save(user2);
1134+
mongoTemplate.save(user3);
1135+
1136+
Person person1 = new Person("u1", "User 1");
1137+
Person person2 = new Person("u2", "User 2");
1138+
1139+
mongoTemplate.save(person1);
1140+
mongoTemplate.save(person2);
1141+
mongoTemplate.save(user3);
1142+
}
1143+
10711144
private void assertLikeStats(LikeStats like, String id, long count) {
10721145

10731146
assertThat(like, is(notNullValue()));

0 commit comments

Comments
 (0)