Skip to content

Commit 4d050f5

Browse files
Polishing.
Reuse Let from VariableOperators. Limit API exposure and favor builders. Update nullability constraints and assertions. Update integration tests. Add unit tests. Original Pull Request: #4272
1 parent 83923e0 commit 4d050f5

File tree

5 files changed

+290
-109
lines changed

5 files changed

+290
-109
lines changed

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

+16-10
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.data.mongodb.core.aggregation.CountOperation.CountOperationBuilder;
2929
import org.springframework.data.mongodb.core.aggregation.FacetOperation.FacetOperationBuilder;
3030
import org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.StartWithBuilder;
31+
import org.springframework.data.mongodb.core.aggregation.LookupOperation.LookupOperationBuilder;
3132
import org.springframework.data.mongodb.core.aggregation.MergeOperation.MergeOperationBuilder;
3233
import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootDocumentOperationBuilder;
3334
import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootOperationBuilder;
@@ -665,16 +666,21 @@ public static LookupOperation lookup(Field from, Field localField, Field foreign
665666
return new LookupOperation(from, localField, foreignField, as);
666667
}
667668

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);
669+
/**
670+
* Entrypoint for creating {@link LookupOperation $lookup} using a fluent builder API.
671+
* <pre class="code">
672+
* Aggregation.lookup().from("restaurants")
673+
* .localField("restaurant_name")
674+
* .foreignField("name")
675+
* .let(newVariable("orders_drink").forField("drink"))
676+
* .pipeline(match(ctx -> new Document("$expr", new Document("$in", List.of("$$orders_drink", "$beverages")))))
677+
* .as("matches")
678+
* </pre>
679+
* @return new instance of {@link LookupOperationBuilder}.
680+
* @since 4.1
681+
*/
682+
public static LookupOperationBuilder lookup() {
683+
return new LookupOperationBuilder();
678684
}
679685

680686
/**

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

+143-65
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,19 @@
1515
*/
1616
package org.springframework.data.mongodb.core.aggregation;
1717

18-
import java.util.List;
18+
import java.util.function.Supplier;
1919

2020
import org.bson.Document;
2121
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
2222
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
23+
import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let;
24+
import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable;
2325
import org.springframework.lang.Nullable;
2426
import org.springframework.util.Assert;
2527

2628
/**
27-
* Encapsulates the aggregation framework {@code $lookup}-operation. We recommend to use the static factory method
28-
* {@link Aggregation#lookup(String, String, String, String)} instead of creating instances of this class directly.
29+
* Encapsulates the aggregation framework {@code $lookup}-operation. We recommend to use the builder provided via
30+
* {@link #newLookup()} instead of creating instances of this class directly.
2931
*
3032
* @author Alessio Fachechi
3133
* @author Christoph Strobl
@@ -37,16 +39,22 @@
3739
*/
3840
public class LookupOperation implements FieldsExposingAggregationOperation, InheritsFieldsAggregationOperation {
3941

40-
private final Field from;
42+
private final String from;
43+
44+
@Nullable //
4145
private final Field localField;
46+
47+
@Nullable //
4248
private final Field foreignField;
43-
private final ExposedField as;
4449

45-
@Nullable
50+
@Nullable //
4651
private final Let let;
47-
@Nullable
52+
53+
@Nullable //
4854
private final AggregationPipeline pipeline;
4955

56+
private final ExposedField as;
57+
5058
/**
5159
* Creates a new {@link LookupOperation} for the given {@link Field}s.
5260
*
@@ -56,13 +64,47 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
5664
* @param as must not be {@literal null}.
5765
*/
5866
public LookupOperation(Field from, Field localField, Field foreignField, Field as) {
59-
this(from, localField, foreignField, as, null, null);
67+
this(((Supplier<String>) () -> {
68+
69+
Assert.notNull(from, "From must not be null");
70+
return from.getTarget();
71+
}).get(), localField, foreignField, null, null, as);
6072
}
6173

62-
public LookupOperation(Field from, Field localField, Field foreignField, Field as, @Nullable Let let, @Nullable AggregationPipeline pipeline) {
74+
/**
75+
* Creates a new {@link LookupOperation} for the given combination of {@link Field}s and {@link AggregationPipeline
76+
* pipeline}.
77+
*
78+
* @param from must not be {@literal null}.
79+
* @param let must not be {@literal null}.
80+
* @param as must not be {@literal null}.
81+
* @since 4.1
82+
*/
83+
public LookupOperation(String from, @Nullable Let let, AggregationPipeline pipeline, Field as) {
84+
this(from, null, null, let, pipeline, as);
85+
}
86+
87+
/**
88+
* Creates a new {@link LookupOperation} for the given combination of {@link Field}s and {@link AggregationPipeline
89+
* pipeline}.
90+
*
91+
* @param from must not be {@literal null}.
92+
* @param localField can be {@literal null} if {@literal pipeline} is present.
93+
* @param foreignField can be {@literal null} if {@literal pipeline} is present.
94+
* @param let can be {@literal null} if {@literal localField} and {@literal foreignField} are present.
95+
* @param as must not be {@literal null}.
96+
* @since 4.1
97+
*/
98+
public LookupOperation(String from, @Nullable Field localField, @Nullable Field foreignField, @Nullable Let let,
99+
@Nullable AggregationPipeline pipeline, Field as) {
100+
63101
Assert.notNull(from, "From must not be null");
64-
Assert.notNull(localField, "LocalField must not be null");
65-
Assert.notNull(foreignField, "ForeignField must not be null");
102+
if (pipeline == null) {
103+
Assert.notNull(localField, "LocalField must not be null");
104+
Assert.notNull(foreignField, "ForeignField must not be null");
105+
} else if (localField == null && foreignField == null) {
106+
Assert.notNull(pipeline, "Pipeline must not be null");
107+
}
66108
Assert.notNull(as, "As must not be null");
67109

68110
this.from = from;
@@ -83,19 +125,22 @@ public Document toDocument(AggregationOperationContext context) {
83125

84126
Document lookupObject = new Document();
85127

86-
lookupObject.append("from", from.getTarget());
87-
lookupObject.append("localField", localField.getTarget());
88-
lookupObject.append("foreignField", foreignField.getTarget());
89-
lookupObject.append("as", as.getTarget());
90-
128+
lookupObject.append("from", from);
129+
if (localField != null) {
130+
lookupObject.append("localField", localField.getTarget());
131+
}
132+
if (foreignField != null) {
133+
lookupObject.append("foreignField", foreignField.getTarget());
134+
}
91135
if (let != null) {
92-
lookupObject.append("let", let.toDocument(context));
136+
lookupObject.append("let", let.toDocument(context).get("$let", Document.class).get("vars"));
93137
}
94-
95138
if (pipeline != null) {
96139
lookupObject.append("pipeline", pipeline.toDocuments(context));
97140
}
98141

142+
lookupObject.append("as", as.getTarget());
143+
99144
return new Document(getOperator(), lookupObject);
100145
}
101146

@@ -122,7 +167,7 @@ public static interface FromBuilder {
122167
LocalFieldBuilder from(String name);
123168
}
124169

125-
public static interface LocalFieldBuilder {
170+
public static interface LocalFieldBuilder extends PipelineBuilder {
126171

127172
/**
128173
* @param name the field from the documents input to the {@code $lookup} stage, must not be {@literal null} or
@@ -141,7 +186,67 @@ public static interface ForeignFieldBuilder {
141186
AsBuilder foreignField(String name);
142187
}
143188

144-
public static interface AsBuilder {
189+
/**
190+
* @since 4.1
191+
* @author Christoph Strobl
192+
*/
193+
public interface LetBuilder {
194+
195+
/**
196+
* Specifies {@link Let#getVariableNames() variables) that can be used in the
197+
* {@link PipelineBuilder#pipeline(AggregationOperation...) pipeline stages}.
198+
*
199+
* @param let must not be {@literal null}.
200+
* @return never {@literal null}.
201+
* @see PipelineBuilder
202+
*/
203+
PipelineBuilder let(Let let);
204+
205+
/**
206+
* Specifies {@link Let#getVariableNames() variables) that can be used in the
207+
* {@link PipelineBuilder#pipeline(AggregationOperation...) pipeline stages}.
208+
*
209+
* @param variables must not be {@literal null}.
210+
* @return never {@literal null}.
211+
* @see PipelineBuilder
212+
*/
213+
default PipelineBuilder let(ExpressionVariable... variables) {
214+
return let(Let.just(variables));
215+
}
216+
}
217+
218+
/**
219+
* @since 4.1
220+
* @author Christoph Strobl
221+
*/
222+
public interface PipelineBuilder extends LetBuilder {
223+
224+
/**
225+
* Specifies the {@link AggregationPipeline pipeline} that determines the resulting documents.
226+
*
227+
* @param pipeline must not be {@literal null}.
228+
* @return never {@literal null}.
229+
*/
230+
AsBuilder pipeline(AggregationPipeline pipeline);
231+
232+
/**
233+
* Specifies the {@link AggregationPipeline#getOperations() stages} that determine the resulting documents.
234+
*
235+
* @param stages must not be {@literal null} can be empty.
236+
* @return never {@literal null}.
237+
*/
238+
default AsBuilder pipeline(AggregationOperation... stages) {
239+
return pipeline(AggregationPipeline.of(stages));
240+
}
241+
242+
/**
243+
* @param name the name of the new array field to add to the input documents, must not be {@literal null} or empty.
244+
* @return new instance of {@link LookupOperation}.
245+
*/
246+
LookupOperation as(String name);
247+
}
248+
249+
public static interface AsBuilder extends PipelineBuilder {
145250

146251
/**
147252
* @param name the name of the new array field to add to the input documents, must not be {@literal null} or empty.
@@ -159,10 +264,12 @@ public static interface AsBuilder {
159264
public static final class LookupOperationBuilder
160265
implements FromBuilder, LocalFieldBuilder, ForeignFieldBuilder, AsBuilder {
161266

162-
private @Nullable Field from;
267+
private @Nullable String from;
163268
private @Nullable Field localField;
164269
private @Nullable Field foreignField;
165270
private @Nullable ExposedField as;
271+
private @Nullable Let let;
272+
private @Nullable AggregationPipeline pipeline;
166273

167274
/**
168275
* Creates new builder for {@link LookupOperation}.
@@ -177,18 +284,10 @@ public static FromBuilder newBuilder() {
177284
public LocalFieldBuilder from(String name) {
178285

179286
Assert.hasText(name, "'From' must not be null or empty");
180-
from = Fields.field(name);
287+
from = name;
181288
return this;
182289
}
183290

184-
@Override
185-
public LookupOperation as(String name) {
186-
187-
Assert.hasText(name, "'As' must not be null or empty");
188-
as = new ExposedField(Fields.field(name), true);
189-
return new LookupOperation(from, localField, foreignField, as);
190-
}
191-
192291
@Override
193292
public AsBuilder foreignField(String name) {
194293

@@ -204,50 +303,29 @@ public ForeignFieldBuilder localField(String name) {
204303
localField = Fields.field(name);
205304
return this;
206305
}
207-
}
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-
}
217306

218307
@Override
219-
public Document toDocument(AggregationOperationContext context) {
220-
return toLet();
221-
}
222-
223-
private Document toLet() {
224-
Document mappedVars = new Document();
308+
public PipelineBuilder let(Let let) {
225309

226-
for (ExpressionVariable var : this.vars) {
227-
mappedVars.putAll(getMappedVariable(var));
228-
}
229-
230-
return mappedVars;
310+
Assert.notNull(let, "Let must not be null");
311+
this.let = let;
312+
return this;
231313
}
232314

233-
private Document getMappedVariable(ExpressionVariable var) {
234-
return new Document(var.variableName, prefixDollarSign(var.expression));
235-
}
315+
@Override
316+
public AsBuilder pipeline(AggregationPipeline pipeline) {
236317

237-
private String prefixDollarSign(String expression) {
238-
return "$" + expression;
318+
Assert.notNull(pipeline, "Pipeline must not be null");
319+
this.pipeline = pipeline;
320+
return this;
239321
}
240322

241-
public static class ExpressionVariable {
242-
243-
private final String variableName;
244-
245-
private final String expression;
323+
@Override
324+
public LookupOperation as(String name) {
246325

247-
public ExpressionVariable(String variableName, String expression) {
248-
this.variableName = variableName;
249-
this.expression = expression;
250-
}
326+
Assert.hasText(name, "'As' must not be null or empty");
327+
as = new ExposedField(Fields.field(name), true);
328+
return new LookupOperation(from, localField, foreignField, let, pipeline, as);
251329
}
252330
}
253331
}

0 commit comments

Comments
 (0)