Skip to content

Commit 460ab05

Browse files
mp911dechristophstrobl
authored andcommitted
DATAMONGO-1391 - Support Mongo 3.2 syntax for $unwind in aggregation.
We now support both, the simple {$unwind: path} and the MongoDB 3.2 {$unwind: {…}} syntax. Original Pull Request: #355
1 parent cf2cad5 commit 460ab05

File tree

6 files changed

+492
-10
lines changed

6 files changed

+492
-10
lines changed

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,56 @@ public static ProjectionOperation project(Fields fields) {
193193
/**
194194
* Factory method to create a new {@link UnwindOperation} for the field with the given name.
195195
*
196-
* @param fieldName must not be {@literal null} or empty.
196+
* @param field must not be {@literal null} or empty.
197197
* @return
198198
*/
199199
public static UnwindOperation unwind(String field) {
200200
return new UnwindOperation(field(field));
201201
}
202202

203+
/**
204+
* Factory method to create a new {@link UnwindOperation} for the field with the given name and
205+
* {@code preserveNullAndEmptyArrays}. Note that extended unwind is supported in MongoDB version 3.2+.
206+
*
207+
* @param field must not be {@literal null} or empty.
208+
* @param preserveNullAndEmptyArrays {@literal true} to output the document if path is {@literal null}, missing or
209+
* array is empty.
210+
* @return
211+
* @since 1.10
212+
*/
213+
public static UnwindOperation unwind(String field, boolean preserveNullAndEmptyArrays) {
214+
return new UnwindOperation(field(field), preserveNullAndEmptyArrays);
215+
}
216+
217+
/**
218+
* Factory method to create a new {@link UnwindOperation} for the field with the given name and to include the index
219+
* field as {@code arrayIndex}. Note that extended unwind is supported in MongoDB version 3.2+.
220+
*
221+
* @param field must not be {@literal null} or empty.
222+
* @param arrayIndex must not be {@literal null} or empty.
223+
* @return
224+
* @since 1.10
225+
*/
226+
public static UnwindOperation unwind(String field, String arrayIndex) {
227+
return new UnwindOperation(field(field), field(arrayIndex), false);
228+
}
229+
230+
/**
231+
* Factory method to create a new {@link UnwindOperation} for the field with the given name, to include the index
232+
* field as {@code arrayIndex} and {@code preserveNullAndEmptyArrays}. Note that extended unwind is supported in
233+
* MongoDB version 3.2+.
234+
*
235+
* @param field must not be {@literal null} or empty.
236+
* @param arrayIndex must not be {@literal null} or empty.
237+
* @param preserveNullAndEmptyArrays {@literal true} to output the document if path is {@literal null}, missing or
238+
* array is empty.
239+
* @return
240+
* @since 1.10
241+
*/
242+
public static UnwindOperation unwind(String field, String arrayIndex, boolean preserveNullAndEmptyArrays) {
243+
return new UnwindOperation(field(field), field(arrayIndex), preserveNullAndEmptyArrays);
244+
}
245+
203246
/**
204247
* Creates a new {@link GroupOperation} for the given fields.
205248
*

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2014 the original author or authors.
2+
* Copyright 2013-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030
*
3131
* @author Oliver Gierke
3232
* @author Thomas Darimont
33+
* @author Mark Paluch
3334
* @since 1.3
3435
*/
3536
public final class ExposedFields implements Iterable<ExposedField> {
@@ -203,8 +204,13 @@ private int exposedFieldsCount() {
203204
public Iterator<ExposedField> iterator() {
204205

205206
CompositeIterator<ExposedField> iterator = new CompositeIterator<ExposedField>();
206-
iterator.add(syntheticFields.iterator());
207-
iterator.add(originalFields.iterator());
207+
if (!syntheticFields.isEmpty()) {
208+
iterator.add(syntheticFields.iterator());
209+
}
210+
211+
if (!originalFields.isEmpty()) {
212+
iterator.add(originalFields.iterator());
213+
}
208214

209215
return iterator;
210216
}

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

Lines changed: 183 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2015 the original author or authors.
2+
* Copyright 2013-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,29 +28,207 @@
2828
* @see http://docs.mongodb.org/manual/reference/aggregation/unwind/#pipe._S_unwind
2929
* @author Thomas Darimont
3030
* @author Oliver Gierke
31+
* @author Mark Paluch
3132
* @since 1.3
3233
*/
33-
public class UnwindOperation implements AggregationOperation {
34+
public class UnwindOperation
35+
implements AggregationOperation, FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation {
3436

3537
private final ExposedField field;
38+
private final ExposedField arrayIndex;
39+
private final boolean preserveNullAndEmptyArrays;
3640

3741
/**
3842
* Creates a new {@link UnwindOperation} for the given {@link Field}.
3943
*
4044
* @param field must not be {@literal null}.
4145
*/
4246
public UnwindOperation(Field field) {
47+
this(new ExposedField(field, true), false);
48+
}
49+
50+
/**
51+
* Creates a new {@link UnwindOperation} using Mongo 3.2 syntax.
52+
*
53+
* @param field must not be {@literal null}.
54+
* @param preserveNullAndEmptyArrays {@literal true} to output the document if path is {@literal null}, missing or
55+
* array is empty.
56+
* @since 1.10
57+
*/
58+
public UnwindOperation(Field field, boolean preserveNullAndEmptyArrays) {
59+
Assert.notNull(field, "Field must not be null!");
60+
61+
this.field = new ExposedField(field, true);
62+
this.arrayIndex = null;
63+
this.preserveNullAndEmptyArrays = preserveNullAndEmptyArrays;
64+
}
65+
66+
/**
67+
* Creates a new {@link UnwindOperation} using Mongo 3.2 syntax.
68+
*
69+
* @param field must not be {@literal null}.
70+
* @param arrayIndex optional field name to expose the field array index, must not be {@literal null}.
71+
* @param preserveNullAndEmptyArrays {@literal true} to output the document if path is {@literal null}, missing or
72+
* array is empty.
73+
* @since 1.10
74+
*/
75+
public UnwindOperation(Field field, Field arrayIndex, boolean preserveNullAndEmptyArrays) {
76+
77+
Assert.notNull(field, "Field must not be null!");
78+
Assert.notNull(arrayIndex, "ArrayIndex must not be null!");
4379

44-
Assert.notNull(field);
4580
this.field = new ExposedField(field, true);
81+
this.arrayIndex = new ExposedField(arrayIndex, true);
82+
this.preserveNullAndEmptyArrays = preserveNullAndEmptyArrays;
4683
}
4784

48-
/*
85+
/*
4986
* (non-Javadoc)
5087
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
5188
*/
5289
@Override
5390
public Document toDocument(AggregationOperationContext context) {
54-
return new Document("$unwind", context.getReference(field).toString());
91+
92+
String unwindField = context.getReference(field).toString();
93+
Object unwindArg;
94+
95+
if (preserveNullAndEmptyArrays || arrayIndex != null) {
96+
97+
Document builder = new Document().append("path", unwindField);
98+
builder = builder.append("preserveNullAndEmptyArrays", preserveNullAndEmptyArrays);
99+
100+
if (arrayIndex != null) {
101+
builder = builder.append("includeArrayIndex", arrayIndex.getName());
102+
}
103+
104+
unwindArg = builder;
105+
} else {
106+
unwindArg = unwindField;
107+
}
108+
109+
return new Document("$unwind", unwindArg);
110+
}
111+
112+
@Override
113+
public ExposedFields getFields() {
114+
return arrayIndex != null ? ExposedFields.from(arrayIndex) : ExposedFields.from();
55115
}
116+
117+
/**
118+
* Get a builder that allows creation of {@link LookupOperation}.
119+
*
120+
* @return
121+
*/
122+
public static PathBuilder newUnwind() {
123+
return UnwindOperationBuilder.newBuilder();
124+
}
125+
126+
public static interface PathBuilder {
127+
128+
/**
129+
* @param path the path to unwind, must not be {@literal null} or empty.
130+
* @return
131+
*/
132+
IndexBuilder path(String path);
133+
}
134+
135+
public static interface IndexBuilder {
136+
137+
/**
138+
* Exposes the array index as {@code field}.
139+
*
140+
* @param field field name to expose the field array index, must not be {@literal null} or empty.
141+
* @return
142+
*/
143+
EmptyArraysBuilder arrayIndex(String field);
144+
145+
/**
146+
* Do not expose the array index.
147+
*
148+
* @return
149+
*/
150+
EmptyArraysBuilder noArrayIndex();
151+
}
152+
153+
public static interface EmptyArraysBuilder {
154+
155+
/**
156+
* Output documents if the array is null or empty.
157+
*
158+
* @return
159+
*/
160+
UnwindOperation preserveNullAndEmptyArrays();
161+
162+
/**
163+
* Do not output documents if the array is null or empty.
164+
*
165+
* @return
166+
*/
167+
UnwindOperation skipNullAndEmptyArrays();
168+
}
169+
170+
/**
171+
* Builder for fluent {@link UnwindOperation} creation.
172+
*
173+
* @author Mark Paluch
174+
* @since 1.10
175+
*/
176+
public static final class UnwindOperationBuilder implements PathBuilder, IndexBuilder, EmptyArraysBuilder {
177+
178+
private Field field;
179+
private Field arrayIndex;
180+
181+
private UnwindOperationBuilder() {}
182+
183+
/**
184+
* Creates new builder for {@link UnwindOperation}.
185+
*
186+
* @return never {@literal null}.
187+
*/
188+
public static PathBuilder newBuilder() {
189+
return new UnwindOperationBuilder();
190+
}
191+
192+
@Override
193+
public UnwindOperation preserveNullAndEmptyArrays() {
194+
195+
if (arrayIndex != null) {
196+
return new UnwindOperation(field, arrayIndex, true);
197+
}
198+
199+
return new UnwindOperation(field, true);
200+
}
201+
202+
@Override
203+
public UnwindOperation skipNullAndEmptyArrays() {
204+
205+
if (arrayIndex != null) {
206+
return new UnwindOperation(field, arrayIndex, false);
207+
}
208+
209+
return new UnwindOperation(field, false);
210+
}
211+
212+
@Override
213+
public EmptyArraysBuilder arrayIndex(String field) {
214+
Assert.hasText(field, "'ArrayIndex' must not be null or empty!");
215+
arrayIndex = Fields.field(field);
216+
return this;
217+
}
218+
219+
@Override
220+
public EmptyArraysBuilder noArrayIndex() {
221+
arrayIndex = null;
222+
return this;
223+
}
224+
225+
@Override
226+
public UnwindOperationBuilder path(String path) {
227+
Assert.hasText(path, "'Path' must not be null or empty!");
228+
field = Fields.field(path);
229+
return this;
230+
}
231+
232+
}
233+
56234
}

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,64 @@ public void shouldAggregateEmptyCollection() {
238238
assertThat(tagCount.size(), is(0));
239239
}
240240

241+
/**
242+
* @see DATAMONGO-1391
243+
*/
244+
@Test
245+
public void shouldUnwindWithIndex() {
246+
247+
MongoCollection<Document> coll = mongoTemplate.getCollection(INPUT_COLLECTION);
248+
249+
coll.insertOne(createDocument("Doc1", "spring", "mongodb", "nosql"));
250+
coll.insertOne(createDocument("Doc2"));
251+
252+
Aggregation agg = newAggregation( //
253+
project("tags"), //
254+
unwind("tags", "n"), //
255+
project("n") //
256+
.and("tag").previousOperation(), //
257+
sort(DESC, "n") //
258+
);
259+
260+
AggregationResults<TagCount> results = mongoTemplate.aggregate(agg, INPUT_COLLECTION, TagCount.class);
261+
262+
assertThat(results, is(notNullValue()));
263+
264+
List<TagCount> tagCount = results.getMappedResults();
265+
266+
assertThat(tagCount, is(notNullValue()));
267+
assertThat(tagCount.size(), is(3));
268+
}
269+
270+
/**
271+
* @see DATAMONGO-1391
272+
*/
273+
@Test
274+
public void shouldUnwindPreserveEmpty() {
275+
276+
MongoCollection<Document> coll = mongoTemplate.getCollection(INPUT_COLLECTION);
277+
278+
coll.insertOne(createDocument("Doc1", "spring", "mongodb", "nosql"));
279+
coll.insertOne(createDocument("Doc2"));
280+
281+
Aggregation agg = newAggregation( //
282+
project("tags"), //
283+
unwind("tags", "n", true), //
284+
sort(DESC, "n") //
285+
);
286+
287+
AggregationResults<Document> results = mongoTemplate.aggregate(agg, INPUT_COLLECTION, Document.class);
288+
289+
assertThat(results, is(notNullValue()));
290+
291+
List<Document> tagCount = results.getMappedResults();
292+
293+
assertThat(tagCount, is(notNullValue()));
294+
assertThat(tagCount.size(), is(4));
295+
assertThat(tagCount.get(0), isBsonObject().containing("n", 2L));
296+
assertThat(tagCount.get(3), isBsonObject().notContaining("n"));
297+
}
298+
241299
@Test
242300
public void shouldDetectResultMismatch() {
243301

0 commit comments

Comments
 (0)