15
15
*/
16
16
package org .springframework .data .mongodb .core .aggregation ;
17
17
18
- import java .util .List ;
18
+ import java .util .function . Supplier ;
19
19
20
20
import org .bson .Document ;
21
21
import org .springframework .data .mongodb .core .aggregation .ExposedFields .ExposedField ;
22
22
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 ;
23
25
import org .springframework .lang .Nullable ;
24
26
import org .springframework .util .Assert ;
25
27
26
28
/**
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.
29
31
*
30
32
* @author Alessio Fachechi
31
33
* @author Christoph Strobl
37
39
*/
38
40
public class LookupOperation implements FieldsExposingAggregationOperation , InheritsFieldsAggregationOperation {
39
41
40
- private final Field from ;
42
+ private final String from ;
43
+
44
+ @ Nullable //
41
45
private final Field localField ;
46
+
47
+ @ Nullable //
42
48
private final Field foreignField ;
43
- private final ExposedField as ;
44
49
45
- @ Nullable
50
+ @ Nullable //
46
51
private final Let let ;
47
- @ Nullable
52
+
53
+ @ Nullable //
48
54
private final AggregationPipeline pipeline ;
49
55
56
+ private final ExposedField as ;
57
+
50
58
/**
51
59
* Creates a new {@link LookupOperation} for the given {@link Field}s.
52
60
*
@@ -56,13 +64,47 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
56
64
* @param as must not be {@literal null}.
57
65
*/
58
66
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 );
60
72
}
61
73
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
+
63
101
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
+ }
66
108
Assert .notNull (as , "As must not be null" );
67
109
68
110
this .from = from ;
@@ -83,19 +125,22 @@ public Document toDocument(AggregationOperationContext context) {
83
125
84
126
Document lookupObject = new Document ();
85
127
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
+ }
91
135
if (let != null ) {
92
- lookupObject .append ("let" , let .toDocument (context ));
136
+ lookupObject .append ("let" , let .toDocument (context ). get ( "$let" , Document . class ). get ( "vars" ) );
93
137
}
94
-
95
138
if (pipeline != null ) {
96
139
lookupObject .append ("pipeline" , pipeline .toDocuments (context ));
97
140
}
98
141
142
+ lookupObject .append ("as" , as .getTarget ());
143
+
99
144
return new Document (getOperator (), lookupObject );
100
145
}
101
146
@@ -122,7 +167,7 @@ public static interface FromBuilder {
122
167
LocalFieldBuilder from (String name );
123
168
}
124
169
125
- public static interface LocalFieldBuilder {
170
+ public static interface LocalFieldBuilder extends PipelineBuilder {
126
171
127
172
/**
128
173
* @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 {
141
186
AsBuilder foreignField (String name );
142
187
}
143
188
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 {
145
250
146
251
/**
147
252
* @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 {
159
264
public static final class LookupOperationBuilder
160
265
implements FromBuilder , LocalFieldBuilder , ForeignFieldBuilder , AsBuilder {
161
266
162
- private @ Nullable Field from ;
267
+ private @ Nullable String from ;
163
268
private @ Nullable Field localField ;
164
269
private @ Nullable Field foreignField ;
165
270
private @ Nullable ExposedField as ;
271
+ private @ Nullable Let let ;
272
+ private @ Nullable AggregationPipeline pipeline ;
166
273
167
274
/**
168
275
* Creates new builder for {@link LookupOperation}.
@@ -177,18 +284,10 @@ public static FromBuilder newBuilder() {
177
284
public LocalFieldBuilder from (String name ) {
178
285
179
286
Assert .hasText (name , "'From' must not be null or empty" );
180
- from = Fields . field ( name ) ;
287
+ from = name ;
181
288
return this ;
182
289
}
183
290
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
-
192
291
@ Override
193
292
public AsBuilder foreignField (String name ) {
194
293
@@ -204,50 +303,29 @@ public ForeignFieldBuilder localField(String name) {
204
303
localField = Fields .field (name );
205
304
return this ;
206
305
}
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
- }
217
306
218
307
@ 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 ) {
225
309
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 ;
231
313
}
232
314
233
- private Document getMappedVariable (ExpressionVariable var ) {
234
- return new Document (var .variableName , prefixDollarSign (var .expression ));
235
- }
315
+ @ Override
316
+ public AsBuilder pipeline (AggregationPipeline pipeline ) {
236
317
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 ;
239
321
}
240
322
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 ) {
246
325
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 );
251
329
}
252
330
}
253
331
}
0 commit comments