Skip to content

Commit 83923e0

Browse files
sangyongchoichristophstrobl
authored andcommitted
Add support for 'let' and 'pipeline' in $lookup
This commit introduces let and pipline to the Lookup aggregation stage. Closes: #3322 Original Pull Request: #4272
1 parent 2558885 commit 83923e0

File tree

3 files changed

+123
-2
lines changed

3 files changed

+123
-2
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
* @author Nikolay Bogdanov
5151
* @author Gustavo de Geus
5252
* @author Jérôme Guyon
53+
* @author Sangyong Choi
5354
* @since 1.3
5455
*/
5556
public class Aggregation {
@@ -664,6 +665,18 @@ public static LookupOperation lookup(Field from, Field localField, Field foreign
664665
return new LookupOperation(from, localField, foreignField, as);
665666
}
666667

668+
public static LookupOperation lookup(String from, String localField, String foreignField, String as, List<AggregationOperation> aggregationOperations) {
669+
return lookup(field(from), field(localField), field(foreignField), field(as), null, new AggregationPipeline(aggregationOperations));
670+
}
671+
672+
public static LookupOperation lookup(String from, String localField, String foreignField, String as, List<LookupOperation.Let.ExpressionVariable> letExpressionVars, List<AggregationOperation> aggregationOperations) {
673+
return lookup(field(from), field(localField), field(foreignField), field(as), new LookupOperation.Let(letExpressionVars), new AggregationPipeline(aggregationOperations));
674+
}
675+
676+
public static LookupOperation lookup(Field from, Field localField, Field foreignField, Field as, LookupOperation.Let let, AggregationPipeline pipeline) {
677+
return new LookupOperation(from, localField, foreignField, as, let, pipeline);
678+
}
679+
667680
/**
668681
* Creates a new {@link CountOperationBuilder}.
669682
*

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.mongodb.core.aggregation;
1717

18+
import java.util.List;
19+
1820
import org.bson.Document;
1921
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
2022
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
@@ -28,6 +30,7 @@
2830
* @author Alessio Fachechi
2931
* @author Christoph Strobl
3032
* @author Mark Paluch
33+
* @author Sangyong Choi
3134
* @since 1.9
3235
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/">MongoDB Aggregation Framework:
3336
* $lookup</a>
@@ -39,6 +42,11 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
3942
private final Field foreignField;
4043
private final ExposedField as;
4144

45+
@Nullable
46+
private final Let let;
47+
@Nullable
48+
private final AggregationPipeline pipeline;
49+
4250
/**
4351
* Creates a new {@link LookupOperation} for the given {@link Field}s.
4452
*
@@ -48,7 +56,10 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
4856
* @param as must not be {@literal null}.
4957
*/
5058
public LookupOperation(Field from, Field localField, Field foreignField, Field as) {
59+
this(from, localField, foreignField, as, null, null);
60+
}
5161

62+
public LookupOperation(Field from, Field localField, Field foreignField, Field as, @Nullable Let let, @Nullable AggregationPipeline pipeline) {
5263
Assert.notNull(from, "From must not be null");
5364
Assert.notNull(localField, "LocalField must not be null");
5465
Assert.notNull(foreignField, "ForeignField must not be null");
@@ -58,6 +69,8 @@ public LookupOperation(Field from, Field localField, Field foreignField, Field a
5869
this.localField = localField;
5970
this.foreignField = foreignField;
6071
this.as = new ExposedField(as, true);
72+
this.let = let;
73+
this.pipeline = pipeline;
6174
}
6275

6376
@Override
@@ -75,6 +88,14 @@ public Document toDocument(AggregationOperationContext context) {
7588
lookupObject.append("foreignField", foreignField.getTarget());
7689
lookupObject.append("as", as.getTarget());
7790

91+
if (let != null) {
92+
lookupObject.append("let", let.toDocument(context));
93+
}
94+
95+
if (pipeline != null) {
96+
lookupObject.append("pipeline", pipeline.toDocuments(context));
97+
}
98+
7899
return new Document(getOperator(), lookupObject);
79100
}
80101

@@ -184,4 +205,49 @@ public ForeignFieldBuilder localField(String name) {
184205
return this;
185206
}
186207
}
208+
209+
public static class Let implements AggregationExpression{
210+
211+
private final List<ExpressionVariable> vars;
212+
213+
public Let(List<ExpressionVariable> vars) {
214+
Assert.notEmpty(vars, "'let' must not be null or empty");
215+
this.vars = vars;
216+
}
217+
218+
@Override
219+
public Document toDocument(AggregationOperationContext context) {
220+
return toLet();
221+
}
222+
223+
private Document toLet() {
224+
Document mappedVars = new Document();
225+
226+
for (ExpressionVariable var : this.vars) {
227+
mappedVars.putAll(getMappedVariable(var));
228+
}
229+
230+
return mappedVars;
231+
}
232+
233+
private Document getMappedVariable(ExpressionVariable var) {
234+
return new Document(var.variableName, prefixDollarSign(var.expression));
235+
}
236+
237+
private String prefixDollarSign(String expression) {
238+
return "$" + expression;
239+
}
240+
241+
public static class ExpressionVariable {
242+
243+
private final String variableName;
244+
245+
private final String expression;
246+
247+
public ExpressionVariable(String variableName, String expression) {
248+
this.variableName = variableName;
249+
this.expression = expression;
250+
}
251+
}
252+
}
187253
}

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

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343

4444
import org.assertj.core.data.Offset;
4545
import org.bson.Document;
46-
import org.bson.types.ObjectId;
4746
import org.junit.jupiter.api.AfterEach;
4847
import org.junit.jupiter.api.BeforeEach;
4948
import org.junit.jupiter.api.Test;
@@ -90,6 +89,7 @@
9089
* @author Maninder Singh
9190
* @author Sergey Shcherbakov
9291
* @author Minsu Kim
92+
* @author Sangyong Choi
9393
*/
9494
@ExtendWith(MongoTemplateExtension.class)
9595
public class AggregationTests {
@@ -499,7 +499,7 @@ void findStatesWithPopulationOver10MillionAggregationExample() {
499499
/*
500500
//complex mongodb aggregation framework example from
501501
https://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state
502-
502+
503503
db.zipcodes.aggregate(
504504
{
505505
$group: {
@@ -1518,6 +1518,48 @@ void shouldLookupPeopleCorectly() {
15181518
assertThat(firstItem).containsEntry("linkedPerson.[0].firstname", "u1");
15191519
}
15201520

1521+
@Test
1522+
void shouldLookupPeopleCorrectlyWithPipeline() {
1523+
createUsersWithReferencedPersons();
1524+
1525+
TypedAggregation<User> agg = newAggregation(User.class, //
1526+
lookup("person", "_id", "firstname", "linkedPerson", List.of(match(where("firstname").is("u1")))), //
1527+
sort(ASC, "id"));
1528+
1529+
AggregationResults<Document> results = mongoTemplate.aggregate(agg, User.class, Document.class);
1530+
1531+
List<Document> mappedResults = results.getMappedResults();
1532+
1533+
Document firstItem = mappedResults.get(0);
1534+
1535+
assertThat(firstItem).containsEntry("_id", "u1");
1536+
assertThat(firstItem).containsEntry("linkedPerson.[0].firstname", "u1");
1537+
}
1538+
1539+
@Test
1540+
void shouldLookupPeopleCorrectlyWithPipelineAndLet() {
1541+
createUsersWithReferencedPersons();
1542+
1543+
TypedAggregation<User> agg = newAggregation(User.class, //
1544+
lookup(
1545+
"person",
1546+
"_id",
1547+
"firstname",
1548+
"linkedPerson",
1549+
List.of(new LookupOperation.Let.ExpressionVariable("personFirstname", "firstname")),
1550+
List.of(match(where("firstname").is("u1")))),
1551+
sort(ASC, "id"));
1552+
1553+
AggregationResults<Document> results = mongoTemplate.aggregate(agg, User.class, Document.class);
1554+
1555+
List<Document> mappedResults = results.getMappedResults();
1556+
1557+
Document firstItem = mappedResults.get(0);
1558+
1559+
assertThat(firstItem).containsEntry("_id", "u1");
1560+
assertThat(firstItem).containsEntry("linkedPerson.[0].firstname", "u1");
1561+
}
1562+
15211563
@Test // DATAMONGO-1326
15221564
void shouldGroupByAndLookupPeopleCorectly() {
15231565

0 commit comments

Comments
 (0)