Skip to content

Commit 3d0750a

Browse files
mp911dechristophstrobl
authored andcommitted
DATAMONGO-1551 - Add $graphLookup aggregation stage.
We now support the $graphLookup aggregation pipeline stage via Aggregation to perform recursive lookup adding the lookup result as array to documents entering $graphLookup. TypedAggregation<Employee> agg = Aggregation.newAggregation(Employee.class, graphLookup("employee") .startWith("reportsTo") .connectFrom("reportsTo") .connectTo("name") .depthField("depth") .maxDepth(5) .as("reportingHierarchy")); Original Pull Request: #424
1 parent 1bf8eb0 commit 3d0750a

File tree

4 files changed

+548
-3
lines changed

4 files changed

+548
-3
lines changed

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

+12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
3030
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
3131
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
32+
import org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.StartWithBuilder;
3233
import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootDocumentOperationBuilder;
3334
import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootOperationBuilder;
3435
import org.springframework.data.mongodb.core.aggregation.Fields.*;
@@ -316,6 +317,17 @@ public static GroupOperation group(Fields fields) {
316317
return new GroupOperation(fields);
317318
}
318319

320+
/**
321+
* Creates a new {@link GraphLookupOperation.FromBuilder} to construct a {@link GraphLookupOperation} given
322+
* {@literal fromCollection}.
323+
*
324+
* @param fromCollection must not be {@literal null} or empty.
325+
* @return
326+
*/
327+
public static StartWithBuilder graphLookup(String fromCollection) {
328+
return GraphLookupOperation.builder().from(fromCollection);
329+
}
330+
319331
/**
320332
* Factory method to create a new {@link SortOperation} for the given {@link Sort}.
321333
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
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+
package org.springframework.data.mongodb.core.aggregation;
17+
18+
import java.util.ArrayList;
19+
import java.util.Arrays;
20+
import java.util.List;
21+
22+
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
23+
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
24+
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
25+
import org.springframework.util.Assert;
26+
27+
import org.bson.Document;
28+
29+
/**
30+
* Encapsulates the aggregation framework {@code $graphLookup}-operation.
31+
* <p>
32+
* Performs a recursive search on a collection, with options for restricting the search by recursion depth and query
33+
* filter.
34+
* <p>
35+
* We recommend to use the static factory method {@link Aggregation#graphLookup(String)} instead of creating instances
36+
* of this class directly.
37+
*
38+
* @see http://docs.mongodb.org/manual/reference/aggregation/graphLookup/
39+
* @author Mark Paluch
40+
* @since 1.10
41+
*/
42+
public class GraphLookupOperation implements InheritsFieldsAggregationOperation {
43+
44+
private final String from;
45+
private final List<Object> startWith;
46+
private final Field connectFrom;
47+
private final Field connectTo;
48+
private final Field as;
49+
private final Long maxDepth;
50+
private final Field depthField;
51+
private final CriteriaDefinition restrictSearchWithMatch;
52+
53+
private GraphLookupOperation(String from, List<Object> startWith, Field connectFrom, Field connectTo, Field as,
54+
Long maxDepth, Field depthField, CriteriaDefinition restrictSearchWithMatch) {
55+
56+
this.from = from;
57+
this.startWith = startWith;
58+
this.connectFrom = connectFrom;
59+
this.connectTo = connectTo;
60+
this.as = as;
61+
this.maxDepth = maxDepth;
62+
this.depthField = depthField;
63+
this.restrictSearchWithMatch = restrictSearchWithMatch;
64+
}
65+
66+
/**
67+
* Creates a new {@link FromBuilder} to build {@link GraphLookupOperation}.
68+
*
69+
* @return a new {@link FromBuilder}.
70+
*/
71+
public static FromBuilder builder() {
72+
return new GraphLookupOperationFromBuilder();
73+
}
74+
75+
/* (non-Javadoc)
76+
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
77+
*/
78+
@Override
79+
public Document toDocument(AggregationOperationContext context) {
80+
81+
Document graphLookup = new Document();
82+
83+
graphLookup.put("from", from);
84+
85+
List<Object> list = new ArrayList<>(startWith.size());
86+
87+
for (Object startWithElement : startWith) {
88+
89+
if (startWithElement instanceof AggregationExpression) {
90+
list.add(((AggregationExpression) startWithElement).toDocument(context));
91+
}
92+
93+
if (startWithElement instanceof Field) {
94+
list.add(context.getReference((Field) startWithElement).toString());
95+
}
96+
}
97+
98+
if (list.size() == 1) {
99+
graphLookup.put("startWith", list.get(0));
100+
} else {
101+
graphLookup.put("startWith", list);
102+
}
103+
104+
graphLookup.put("connectFromField", connectFrom.getName());
105+
graphLookup.put("connectToField", connectTo.getName());
106+
graphLookup.put("as", as.getName());
107+
108+
if (maxDepth != null) {
109+
graphLookup.put("maxDepth", maxDepth);
110+
}
111+
112+
if (depthField != null) {
113+
graphLookup.put("depthField", depthField.getName());
114+
}
115+
116+
if (restrictSearchWithMatch != null) {
117+
graphLookup.put("restrictSearchWithMatch", context.getMappedObject(restrictSearchWithMatch.getCriteriaObject()));
118+
}
119+
120+
return new Document("$graphLookup", graphLookup);
121+
}
122+
123+
/*
124+
* (non-Javadoc)
125+
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields()
126+
*/
127+
@Override
128+
public ExposedFields getFields() {
129+
return ExposedFields.from(new ExposedField(as, true));
130+
}
131+
132+
/**
133+
* @author Mark Paluch
134+
*/
135+
public interface FromBuilder {
136+
137+
/**
138+
* Set the {@literal collectionName} to apply the {@code $graphLookup} to.
139+
*
140+
* @param collectionName must not be {@literal null} or empty.
141+
* @return
142+
*/
143+
StartWithBuilder from(String collectionName);
144+
}
145+
146+
/**
147+
* @author Mark Paluch
148+
*/
149+
public interface StartWithBuilder {
150+
151+
/**
152+
* Set the startWith {@literal fieldReferences} to apply the {@code $graphLookup} to.
153+
*
154+
* @param fieldReferences must not be {@literal null}.
155+
* @return
156+
*/
157+
ConnectFromBuilder startWith(String... fieldReferences);
158+
159+
/**
160+
* Set the startWith {@literal expressions} to apply the {@code $graphLookup} to.
161+
*
162+
* @param expressions must not be {@literal null}.
163+
* @return
164+
*/
165+
ConnectFromBuilder startWith(AggregationExpression... expressions);
166+
}
167+
168+
/**
169+
* @author Mark Paluch
170+
*/
171+
public interface ConnectFromBuilder {
172+
173+
/**
174+
* Set the connectFrom {@literal fieldName} to apply the {@code $graphLookup} to.
175+
*
176+
* @param fieldName must not be {@literal null} or empty.
177+
* @return
178+
*/
179+
ConnectToBuilder connectFrom(String fieldName);
180+
}
181+
182+
/**
183+
* @author Mark Paluch
184+
*/
185+
public interface ConnectToBuilder {
186+
187+
/**
188+
* Set the connectTo {@literal fieldName} to apply the {@code $graphLookup} to.
189+
*
190+
* @param fieldName must not be {@literal null} or empty.
191+
* @return
192+
*/
193+
GraphLookupOperationBuilder connectTo(String fieldName);
194+
}
195+
196+
/**
197+
* Builder to build the initial {@link GraphLookupOperationBuilder} that configures the initial mandatory set of
198+
* {@link GraphLookupOperation} properties.
199+
*
200+
* @author Mark Paluch
201+
*/
202+
static final class GraphLookupOperationFromBuilder
203+
implements FromBuilder, StartWithBuilder, ConnectFromBuilder, ConnectToBuilder {
204+
205+
private String from;
206+
private List<? extends Object> startWith;
207+
private String connectFrom;
208+
209+
/* (non-Javadoc)
210+
* @see org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.FromBuilder#from(java.lang.String)
211+
*/
212+
@Override
213+
public StartWithBuilder from(String collectionName) {
214+
215+
Assert.hasText(collectionName, "CollectionName must not be null or empty!");
216+
217+
this.from = collectionName;
218+
219+
return this;
220+
}
221+
222+
/* (non-Javadoc)
223+
* @see org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.StartWithBuilder#startWith(java.lang.String[])
224+
*/
225+
@Override
226+
public ConnectFromBuilder startWith(String... fieldReferences) {
227+
228+
Assert.notNull(fieldReferences, "FieldReferences must not be null!");
229+
Assert.noNullElements(fieldReferences, "FieldReferences must not contain null elements!");
230+
231+
List<Object> fields = new ArrayList<Object>(fieldReferences.length);
232+
233+
for (String fieldReference : fieldReferences) {
234+
fields.add(Fields.field(fieldReference));
235+
}
236+
237+
this.startWith = fields;
238+
239+
return this;
240+
}
241+
242+
/* (non-Javadoc)
243+
* @see org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.StartWithBuilder#startWith(org.springframework.data.mongodb.core.aggregation.AggregationExpression[])
244+
*/
245+
@Override
246+
public ConnectFromBuilder startWith(AggregationExpression... expressions) {
247+
248+
Assert.notNull(expressions, "AggregationExpressions must not be null!");
249+
Assert.noNullElements(expressions, "AggregationExpressions must not contain null elements!");
250+
251+
this.startWith = Arrays.asList(expressions);
252+
253+
return this;
254+
}
255+
256+
/* (non-Javadoc)
257+
* @see org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.ConnectFromBuilder#connectFrom(java.lang.String)
258+
*/
259+
@Override
260+
public ConnectToBuilder connectFrom(String fieldName) {
261+
262+
Assert.hasText(fieldName, "ConnectFrom must not be null or empty!");
263+
264+
this.connectFrom = fieldName;
265+
266+
return this;
267+
}
268+
269+
/* (non-Javadoc)
270+
* @see org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.ConnectToBuilder#connectTo(java.lang.String)
271+
*/
272+
@Override
273+
public GraphLookupOperationBuilder connectTo(String fieldName) {
274+
275+
Assert.hasText(fieldName, "ConnectTo must not be null or empty!");
276+
277+
return new GraphLookupOperationBuilder(from, startWith, connectFrom, fieldName);
278+
}
279+
}
280+
281+
/**
282+
* @author Mark Paluch
283+
*/
284+
static final class GraphLookupOperationBuilder {
285+
286+
private final String from;
287+
private final List<Object> startWith;
288+
private final Field connectFrom;
289+
private final Field connectTo;
290+
private Long maxDepth;
291+
private Field depthField;
292+
private CriteriaDefinition restrictSearchWithMatch;
293+
294+
protected GraphLookupOperationBuilder(String from, List<? extends Object> startWith, String connectFrom,
295+
String connectTo) {
296+
297+
this.from = from;
298+
this.startWith = new ArrayList<Object>(startWith);
299+
this.connectFrom = Fields.field(connectFrom);
300+
this.connectTo = Fields.field(connectTo);
301+
}
302+
303+
/**
304+
* Limit the number of recursions.
305+
*
306+
* @param numberOfRecursions must be greater or equal to zero.
307+
* @return
308+
*/
309+
public GraphLookupOperationBuilder maxDepth(long numberOfRecursions) {
310+
311+
Assert.isTrue(numberOfRecursions >= 0, "Max depth must be >= 0!");
312+
313+
this.maxDepth = numberOfRecursions;
314+
315+
return this;
316+
}
317+
318+
/**
319+
* Add a depth field {@literal fieldName} to each traversed document in the search path.
320+
*
321+
* @param fieldName must not be {@literal null} or empty.
322+
* @return
323+
*/
324+
public GraphLookupOperationBuilder depthField(String fieldName) {
325+
326+
Assert.hasText(fieldName, "Depth field name must not be null or empty!");
327+
328+
this.depthField = Fields.field(fieldName);
329+
330+
return this;
331+
}
332+
333+
/**
334+
* Add a query specifying conditions to the recursive search.
335+
*
336+
* @param criteriaDefinition must not be {@literal null}.
337+
* @return
338+
*/
339+
public GraphLookupOperationBuilder restrict(CriteriaDefinition criteriaDefinition) {
340+
341+
Assert.notNull(criteriaDefinition, "CriteriaDefinition must not be null!");
342+
343+
this.restrictSearchWithMatch = criteriaDefinition;
344+
345+
return this;
346+
}
347+
348+
/**
349+
* Set the name of the array field added to each output document and return the final {@link GraphLookupOperation}.
350+
* Contains the documents traversed in the {@literal $graphLookup} stage to reach the document.
351+
*
352+
* @param fieldName must not be {@literal null} or empty.
353+
* @return the final {@link GraphLookupOperation}.
354+
*/
355+
public GraphLookupOperation as(String fieldName) {
356+
357+
Assert.hasText(fieldName, "As field name must not be null or empty!");
358+
359+
return new GraphLookupOperation(from, startWith, connectFrom, connectTo, Fields.field(fieldName), maxDepth,
360+
depthField, restrictSearchWithMatch);
361+
}
362+
}
363+
}

0 commit comments

Comments
 (0)