Skip to content

Commit f8681fe

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 0dd9048 commit f8681fe

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
@@ -195,13 +195,56 @@ public static ProjectionOperation project(Fields fields) {
195195
/**
196196
* Factory method to create a new {@link UnwindOperation} for the field with the given name.
197197
*
198-
* @param fieldName must not be {@literal null} or empty.
198+
* @param field must not be {@literal null} or empty.
199199
* @return
200200
*/
201201
public static UnwindOperation unwind(String field) {
202202
return new UnwindOperation(field(field));
203203
}
204204

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

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: 184 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.
@@ -19,6 +19,7 @@
1919
import org.springframework.util.Assert;
2020

2121
import com.mongodb.BasicDBObject;
22+
import com.mongodb.BasicDBObjectBuilder;
2223
import com.mongodb.DBObject;
2324

2425
/**
@@ -30,29 +31,207 @@
3031
* @see http://docs.mongodb.org/manual/reference/aggregation/unwind/#pipe._S_unwind
3132
* @author Thomas Darimont
3233
* @author Oliver Gierke
34+
* @author Mark Paluch
3335
* @since 1.3
3436
*/
35-
public class UnwindOperation implements AggregationOperation {
37+
public class UnwindOperation
38+
implements AggregationOperation, FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation {
3639

3740
private final ExposedField field;
41+
private final ExposedField arrayIndex;
42+
private final boolean preserveNullAndEmptyArrays;
3843

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

46-
Assert.notNull(field);
4783
this.field = new ExposedField(field, true);
84+
this.arrayIndex = new ExposedField(arrayIndex, true);
85+
this.preserveNullAndEmptyArrays = preserveNullAndEmptyArrays;
4886
}
4987

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

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
@@ -241,6 +241,64 @@ public void shouldAggregateEmptyCollection() {
241241
assertThat(tagCount.size(), is(0));
242242
}
243243

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

0 commit comments

Comments
 (0)