Skip to content

Commit cadcbf6

Browse files
Thomas Darimontodrotbohm
Thomas Darimont
authored andcommitted
DATAMONGO-954 - Add support for system variables in aggregation operations.
We now support referring to system variables like for instance $$ROOT or $$CURRENT from within aggregation framework pipeline projection and group expressions. Original pull request: spring-projects#190.
1 parent 118f007 commit cadcbf6

File tree

6 files changed

+174
-3
lines changed

6 files changed

+174
-3
lines changed

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

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,22 @@
4444
*/
4545
public class Aggregation {
4646

47+
/**
48+
* References the root document, i.e. the top-level document, currently being processed in the aggregation pipeline
49+
* stage.
50+
*/
51+
public static final String ROOT = SystemVariable.ROOT.toString();
52+
53+
/**
54+
* References the start of the field path being processed in the aggregation pipeline stage. Unless documented
55+
* otherwise, all stages start with CURRENT the same as ROOT.
56+
*/
57+
public static final String CURRENT = SystemVariable.CURRENT.toString();
58+
4759
public static final AggregationOperationContext DEFAULT_CONTEXT = new NoOpAggregationOperationContext();
4860
public static final AggregationOptions DEFAULT_OPTIONS = newAggregationOptions().build();
4961

5062
protected final List<AggregationOperation> operations;
51-
5263
private final AggregationOptions options;
5364

5465
/**
@@ -363,4 +374,51 @@ public FieldReference getReference(String name) {
363374
return new FieldReference(new ExposedField(new AggregationField(name), true));
364375
}
365376
}
377+
378+
/**
379+
* Describes the system variables available in MongoDB aggregation framework pipeline expressions.
380+
*
381+
* @author Thomas Darimont
382+
* @see http://docs.mongodb.org/manual/reference/aggregation-variables
383+
*/
384+
enum SystemVariable {
385+
386+
ROOT, CURRENT;
387+
388+
private static final String PREFIX = "$$";
389+
390+
/**
391+
* Return {@literal true} if the given {@code fieldRef} denotes a well-known system variable, {@literal false}
392+
* otherwise.
393+
*
394+
* @param fieldRef may be {@literal null}.
395+
* @return
396+
*/
397+
public static boolean isReferingToSystemVariable(String fieldRef) {
398+
399+
if (fieldRef == null || !fieldRef.startsWith(PREFIX) || fieldRef.length() <= 2) {
400+
return false;
401+
}
402+
403+
int indexOfFirstDot = fieldRef.indexOf('.');
404+
String candidate = fieldRef.substring(2, indexOfFirstDot == -1 ? fieldRef.length() : indexOfFirstDot);
405+
406+
for (SystemVariable value : values()) {
407+
if (value.name().equals(candidate)) {
408+
return true;
409+
}
410+
}
411+
412+
return false;
413+
}
414+
415+
/*
416+
* (non-Javadoc)
417+
* @see java.lang.Enum#toString()
418+
*/
419+
@Override
420+
public String toString() {
421+
return PREFIX.concat(name());
422+
}
423+
}
366424
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* Value object to capture a list of {@link Field} instances.
3131
*
3232
* @author Oliver Gierke
33+
* @author Thomas Darimont
3334
* @since 1.3
3435
*/
3536
public final class Fields implements Iterable<Field> {
@@ -186,7 +187,7 @@ static class AggregationField implements Field {
186187
private final String target;
187188

188189
/**
189-
* Creates an aggregation fieldwith the given name. As no target is set explicitly, the name will be used as target
190+
* Creates an aggregation field with the given name. As no target is set explicitly, the name will be used as target
190191
* as well.
191192
*
192193
* @param key
@@ -217,6 +218,10 @@ private static final String cleanUp(String source) {
217218
return source;
218219
}
219220

221+
if (Aggregation.SystemVariable.isReferingToSystemVariable(source)) {
222+
return source;
223+
}
224+
220225
int dollarIndex = source.lastIndexOf('$');
221226
return dollarIndex == -1 ? source : source.substring(dollarIndex + 1);
222227
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,16 @@ public DBObject toDBObject(AggregationOperationContext context) {
364364
}
365365

366366
public Object getValue(AggregationOperationContext context) {
367-
return reference == null ? value : context.getReference(reference).toString();
367+
368+
if (reference == null) {
369+
return value;
370+
}
371+
372+
if (Aggregation.SystemVariable.isReferingToSystemVariable(reference)) {
373+
return reference;
374+
}
375+
376+
return context.getReference(reference).toString();
368377
}
369378

370379
@Override

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,10 @@ private Object renderFieldValue(AggregationOperationContext context) {
627627
// implicit reference or explicit include?
628628
if (value == null || Boolean.TRUE.equals(value)) {
629629

630+
if (Aggregation.SystemVariable.isReferingToSystemVariable(field.getTarget())) {
631+
return field.getTarget();
632+
}
633+
630634
// check whether referenced field exists in the context
631635
return context.getReference(field).getReferenceValue();
632636

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@
4444
import org.springframework.core.io.ClassPathResource;
4545
import org.springframework.dao.DataAccessException;
4646
import org.springframework.data.annotation.Id;
47+
import org.springframework.data.domain.Sort.Direction;
4748
import org.springframework.data.mapping.model.MappingException;
4849
import org.springframework.data.mongodb.core.CollectionCallback;
4950
import org.springframework.data.mongodb.core.MongoTemplate;
5051
import org.springframework.data.mongodb.core.aggregation.AggregationTests.CarDescriptor.Entry;
5152
import org.springframework.data.mongodb.core.query.Query;
53+
import org.springframework.data.mongodb.repository.Person;
5254
import org.springframework.data.util.Version;
5355
import org.springframework.test.context.ContextConfiguration;
5456
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -113,6 +115,8 @@ private void cleanDb() {
113115
mongoTemplate.dropCollection(Data.class);
114116
mongoTemplate.dropCollection(DATAMONGO788.class);
115117
mongoTemplate.dropCollection(User.class);
118+
mongoTemplate.dropCollection(Person.class);
119+
mongoTemplate.dropCollection(Reservation.class);
116120
}
117121

118122
/**
@@ -903,6 +907,55 @@ public void returnFiveMostCommonLikesShouldReturnStageExecutionInformationWithEx
903907
assertThat(rawResult.containsField("stages"), is(true));
904908
}
905909

910+
/**
911+
* @see DATAMONGO-954
912+
*/
913+
@Test
914+
public void shouldSupportReturningCurrentAggregationRoot() {
915+
916+
mongoTemplate.save(new Person("p1_first", "p1_last", 25));
917+
mongoTemplate.save(new Person("p2_first", "p2_last", 32));
918+
mongoTemplate.save(new Person("p3_first", "p3_last", 25));
919+
mongoTemplate.save(new Person("p4_first", "p4_last", 15));
920+
921+
List<DBObject> personsWithAge25 = mongoTemplate.find(Query.query(where("age").is(25)), DBObject.class,
922+
mongoTemplate.getCollectionName(Person.class));
923+
924+
Aggregation agg = newAggregation(group("age").push(Aggregation.ROOT).as("users"));
925+
AggregationResults<DBObject> result = mongoTemplate.aggregate(agg, Person.class, DBObject.class);
926+
927+
assertThat(result.getMappedResults(), hasSize(3));
928+
DBObject o = (DBObject) result.getMappedResults().get(2);
929+
930+
assertThat(o.get("_id"), is((Object) 25));
931+
assertThat((List<?>) o.get("users"), hasSize(2));
932+
assertThat((List<?>) o.get("users"), is(contains(personsWithAge25.toArray())));
933+
}
934+
935+
/**
936+
* @see DATAMONGO-954
937+
* @see http
938+
* ://stackoverflow.com/questions/24185987/using-root-inside-spring-data-mongodb-for-retrieving-whole-document
939+
*/
940+
@Test
941+
public void shouldSupportReturningCurrentAggregationRootInReference() {
942+
943+
mongoTemplate.save(new Reservation("0123", "42", 100));
944+
mongoTemplate.save(new Reservation("0360", "43", 200));
945+
mongoTemplate.save(new Reservation("0360", "44", 300));
946+
947+
Aggregation agg = newAggregation( //
948+
match(where("hotelCode").is("0360")), //
949+
sort(Direction.DESC, "confirmationNumber", "timestamp"), //
950+
group("confirmationNumber") //
951+
.first("timestamp").as("timestamp") //
952+
.first(Aggregation.ROOT).as("reservationImage") //
953+
);
954+
AggregationResults<DBObject> result = mongoTemplate.aggregate(agg, Reservation.class, DBObject.class);
955+
956+
assertThat(result.getMappedResults(), hasSize(2));
957+
}
958+
906959
private void assertLikeStats(LikeStats like, String id, long count) {
907960

908961
assertThat(like, is(notNullValue()));
@@ -1067,4 +1120,19 @@ public Entry(String make, String model, int year) {
10671120
}
10681121
}
10691122
}
1123+
1124+
static class Reservation {
1125+
1126+
String hotelCode;
1127+
String confirmationNumber;
1128+
int timestamp;
1129+
1130+
public Reservation() {}
1131+
1132+
public Reservation(String hotelCode, String confirmationNumber, int timestamp) {
1133+
this.hotelCode = hotelCode;
1134+
this.confirmationNumber = confirmationNumber;
1135+
this.timestamp = timestamp;
1136+
}
1137+
}
10701138
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.junit.Rule;
2828
import org.junit.Test;
2929
import org.junit.rules.ExpectedException;
30+
import org.springframework.data.domain.Sort.Direction;
3031

3132
import com.mongodb.BasicDBObject;
3233
import com.mongodb.DBObject;
@@ -256,6 +257,32 @@ public void shouldRenderAggregationWithCustomOptionsCorrectly() {
256257
));
257258
}
258259

260+
/**
261+
* @see DATAMONGO-954
262+
*/
263+
@Test
264+
public void shouldSupportReferencingSystemVariables() {
265+
266+
DBObject agg = newAggregation( //
267+
project("someKey") //
268+
.and("a").as("a1") //
269+
.and(Aggregation.CURRENT + ".a").as("a2") //
270+
, sort(Direction.DESC, "a") //
271+
, group("someKey").first(Aggregation.ROOT).as("doc") //
272+
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
273+
274+
DBObject projection0 = extractPipelineElement(agg, 0, "$project");
275+
assertThat(projection0, is((DBObject) new BasicDBObject("someKey", 1).append("a1", "$a")
276+
.append("a2", "$$CURRENT.a")));
277+
278+
DBObject sort = extractPipelineElement(agg, 1, "$sort");
279+
assertThat(sort, is((DBObject) new BasicDBObject("a", -1)));
280+
281+
DBObject group = extractPipelineElement(agg, 2, "$group");
282+
assertThat(group,
283+
is((DBObject) new BasicDBObject("_id", "$someKey").append("doc", new BasicDBObject("$first", "$$ROOT"))));
284+
}
285+
259286
private DBObject extractPipelineElement(DBObject agg, int index, String operation) {
260287

261288
List<DBObject> pipeline = (List<DBObject>) agg.get("pipeline");

0 commit comments

Comments
 (0)