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
This commit is contained in:
Mark Paluch
2016-03-29 14:32:45 +02:00
committed by Christoph Strobl
parent 0dd904894d
commit f8681fec66
6 changed files with 494 additions and 12 deletions

View File

@@ -195,13 +195,56 @@ public class Aggregation {
/**
* Factory method to create a new {@link UnwindOperation} for the field with the given name.
*
* @param fieldName must not be {@literal null} or empty.
* @param field must not be {@literal null} or empty.
* @return
*/
public static UnwindOperation unwind(String field) {
return new UnwindOperation(field(field));
}
/**
* Factory method to create a new {@link UnwindOperation} for the field with the given name and
* {@code preserveNullAndEmptyArrays}. Note that extended unwind is supported in MongoDB version 3.2+.
*
* @param field must not be {@literal null} or empty.
* @param preserveNullAndEmptyArrays {@literal true} to output the document if path is {@literal null}, missing or
* array is empty.
* @return
* @since 1.10
*/
public static UnwindOperation unwind(String field, boolean preserveNullAndEmptyArrays) {
return new UnwindOperation(field(field), preserveNullAndEmptyArrays);
}
/**
* Factory method to create a new {@link UnwindOperation} for the field with the given name and to include the index
* field as {@code arrayIndex}. Note that extended unwind is supported in MongoDB version 3.2+.
*
* @param field must not be {@literal null} or empty.
* @param arrayIndex must not be {@literal null} or empty.
* @return
* @since 1.10
*/
public static UnwindOperation unwind(String field, String arrayIndex) {
return new UnwindOperation(field(field), field(arrayIndex), false);
}
/**
* Factory method to create a new {@link UnwindOperation} for the field with the given name, to include the index
* field as {@code arrayIndex} and {@code preserveNullAndEmptyArrays}. Note that extended unwind is supported in
* MongoDB version 3.2+.
*
* @param field must not be {@literal null} or empty.
* @param arrayIndex must not be {@literal null} or empty.
* @param preserveNullAndEmptyArrays {@literal true} to output the document if path is {@literal null}, missing or
* array is empty.
* @return
* @since 1.10
*/
public static UnwindOperation unwind(String field, String arrayIndex, boolean preserveNullAndEmptyArrays) {
return new UnwindOperation(field(field), field(arrayIndex), preserveNullAndEmptyArrays);
}
/**
* Creates a new {@link GroupOperation} for the given fields.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2014 the original author or authors.
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import org.springframework.util.CompositeIterator;
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Mark Paluch
* @since 1.3
*/
public final class ExposedFields implements Iterable<ExposedField> {
@@ -203,8 +204,13 @@ public final class ExposedFields implements Iterable<ExposedField> {
public Iterator<ExposedField> iterator() {
CompositeIterator<ExposedField> iterator = new CompositeIterator<ExposedField>();
iterator.add(syntheticFields.iterator());
iterator.add(originalFields.iterator());
if (!syntheticFields.isEmpty()) {
iterator.add(syntheticFields.iterator());
}
if (!originalFields.isEmpty()) {
iterator.add(originalFields.iterator());
}
return iterator;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedFi
import org.springframework.util.Assert;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBObject;
/**
@@ -30,11 +31,15 @@ import com.mongodb.DBObject;
* @see http://docs.mongodb.org/manual/reference/aggregation/unwind/#pipe._S_unwind
* @author Thomas Darimont
* @author Oliver Gierke
* @author Mark Paluch
* @since 1.3
*/
public class UnwindOperation implements AggregationOperation {
public class UnwindOperation
implements AggregationOperation, FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation {
private final ExposedField field;
private final ExposedField arrayIndex;
private final boolean preserveNullAndEmptyArrays;
/**
* Creates a new {@link UnwindOperation} for the given {@link Field}.
@@ -42,17 +47,191 @@ public class UnwindOperation implements AggregationOperation {
* @param field must not be {@literal null}.
*/
public UnwindOperation(Field field) {
Assert.notNull(field);
this.field = new ExposedField(field, true);
this(new ExposedField(field, true), false);
}
/*
/**
* Creates a new {@link UnwindOperation} using Mongo 3.2 syntax.
*
* @param field must not be {@literal null}.
* @param preserveNullAndEmptyArrays {@literal true} to output the document if path is {@literal null}, missing or
* array is empty.
* @since 1.10
*/
public UnwindOperation(Field field, boolean preserveNullAndEmptyArrays) {
Assert.notNull(field, "Field must not be null!");
this.field = new ExposedField(field, true);
this.arrayIndex = null;
this.preserveNullAndEmptyArrays = preserveNullAndEmptyArrays;
}
/**
* Creates a new {@link UnwindOperation} using Mongo 3.2 syntax.
*
* @param field must not be {@literal null}.
* @param arrayIndex optional field name to expose the field array index, must not be {@literal null}.
* @param preserveNullAndEmptyArrays {@literal true} to output the document if path is {@literal null}, missing or
* array is empty.
* @since 1.10
*/
public UnwindOperation(Field field, Field arrayIndex, boolean preserveNullAndEmptyArrays) {
Assert.notNull(field, "Field must not be null!");
Assert.notNull(arrayIndex, "ArrayIndex must not be null!");
this.field = new ExposedField(field, true);
this.arrayIndex = new ExposedField(arrayIndex, true);
this.preserveNullAndEmptyArrays = preserveNullAndEmptyArrays;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public DBObject toDBObject(AggregationOperationContext context) {
return new BasicDBObject("$unwind", context.getReference(field).toString());
String unwindField = context.getReference(field).toString();
Object unwindArg;
if (preserveNullAndEmptyArrays || arrayIndex != null) {
BasicDBObjectBuilder builder = BasicDBObjectBuilder.start().add("path", unwindField);
builder.add("preserveNullAndEmptyArrays", preserveNullAndEmptyArrays);
if (arrayIndex != null) {
builder.add("includeArrayIndex", arrayIndex.getName());
}
unwindArg = builder.get();
} else {
unwindArg = unwindField;
}
return new BasicDBObject("$unwind", unwindArg);
}
@Override
public ExposedFields getFields() {
return arrayIndex != null ? ExposedFields.from(arrayIndex) : ExposedFields.from();
}
/**
* Get a builder that allows creation of {@link LookupOperation}.
*
* @return
*/
public static PathBuilder newUnwind() {
return UnwindOperationBuilder.newBuilder();
}
public static interface PathBuilder {
/**
* @param path the path to unwind, must not be {@literal null} or empty.
* @return
*/
IndexBuilder path(String path);
}
public static interface IndexBuilder {
/**
* Exposes the array index as {@code field}.
*
* @param field field name to expose the field array index, must not be {@literal null} or empty.
* @return
*/
EmptyArraysBuilder arrayIndex(String field);
/**
* Do not expose the array index.
*
* @return
*/
EmptyArraysBuilder noArrayIndex();
}
public static interface EmptyArraysBuilder {
/**
* Output documents if the array is null or empty.
*
* @return
*/
UnwindOperation preserveNullAndEmptyArrays();
/**
* Do not output documents if the array is null or empty.
*
* @return
*/
UnwindOperation skipNullAndEmptyArrays();
}
/**
* Builder for fluent {@link UnwindOperation} creation.
*
* @author Mark Paluch
* @since 1.10
*/
public static final class UnwindOperationBuilder implements PathBuilder, IndexBuilder, EmptyArraysBuilder {
private Field field;
private Field arrayIndex;
private UnwindOperationBuilder() {}
/**
* Creates new builder for {@link UnwindOperation}.
*
* @return never {@literal null}.
*/
public static PathBuilder newBuilder() {
return new UnwindOperationBuilder();
}
@Override
public UnwindOperation preserveNullAndEmptyArrays() {
if (arrayIndex != null) {
return new UnwindOperation(field, arrayIndex, true);
}
return new UnwindOperation(field, true);
}
@Override
public UnwindOperation skipNullAndEmptyArrays() {
if (arrayIndex != null) {
return new UnwindOperation(field, arrayIndex, false);
}
return new UnwindOperation(field, false);
}
@Override
public EmptyArraysBuilder arrayIndex(String field) {
Assert.hasText(field, "'ArrayIndex' must not be null or empty!");
arrayIndex = Fields.field(field);
return this;
}
@Override
public EmptyArraysBuilder noArrayIndex() {
arrayIndex = null;
return this;
}
@Override
public UnwindOperationBuilder path(String path) {
Assert.hasText(path, "'Path' must not be null or empty!");
field = Fields.field(path);
return this;
}
}
}

View File

@@ -241,6 +241,64 @@ public class AggregationTests {
assertThat(tagCount.size(), is(0));
}
/**
* @see DATAMONGO-1391
*/
@Test
public void shouldUnwindWithIndex() {
DBCollection coll = mongoTemplate.getCollection(INPUT_COLLECTION);
coll.insert(createDocument("Doc1", "spring", "mongodb", "nosql"));
coll.insert(createDocument("Doc2"));
Aggregation agg = newAggregation( //
project("tags"), //
unwind("tags", "n"), //
project("n") //
.and("tag").previousOperation(), //
sort(DESC, "n") //
);
AggregationResults<TagCount> results = mongoTemplate.aggregate(agg, INPUT_COLLECTION, TagCount.class);
assertThat(results, is(notNullValue()));
List<TagCount> tagCount = results.getMappedResults();
assertThat(tagCount, is(notNullValue()));
assertThat(tagCount.size(), is(3));
}
/**
* @see DATAMONGO-1391
*/
@Test
public void shouldUnwindPreserveEmpty() {
DBCollection coll = mongoTemplate.getCollection(INPUT_COLLECTION);
coll.insert(createDocument("Doc1", "spring", "mongodb", "nosql"));
coll.insert(createDocument("Doc2"));
Aggregation agg = newAggregation( //
project("tags"), //
unwind("tags", "n", true), //
sort(DESC, "n") //
);
AggregationResults<DBObject> results = mongoTemplate.aggregate(agg, INPUT_COLLECTION, DBObject.class);
assertThat(results, is(notNullValue()));
List<DBObject> tagCount = results.getMappedResults();
assertThat(tagCount, is(notNullValue()));
assertThat(tagCount.size(), is(4));
assertThat(tagCount.get(0), isBsonObject().containing("n", 2L));
assertThat(tagCount.get(3), isBsonObject().notContaining("n"));
}
@Test
public void shouldDetectResultMismatch() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import static org.junit.Assert.*;
import static org.springframework.data.mongodb.core.DBObjectTestUtils.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
import java.util.ArrayList;
import java.util.List;
@@ -39,6 +40,7 @@ import com.mongodb.DBObject;
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
*/
public class AggregationUnitTests {
@@ -94,6 +96,64 @@ public class AggregationUnitTests {
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
}
/**
* @see DATAMONGO-1391
*/
@Test
public void unwindOperationWithIndexShouldPreserveFields() {
newAggregation( //
project("a", "b"), //
unwind("a", "x"), //
project("a", "b") // b should still be available
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
}
/**
* @see DATAMONGO-1391
*/
@Test
public void unwindOperationWithIndexShouldAddIndexField() {
newAggregation( //
project("a", "b"), //
unwind("a", "x"), //
project("a", "x") // b should still be available
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
}
/**
* @see DATAMONGO-1391
*/
@Test
public void fullUnwindOperationShouldBuildCorrectClause() {
DBObject agg = newAggregation( //
unwind("a", "x", true)).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
DBObject unwind = ((List<DBObject>) agg.get("pipeline")).get(0);
assertThat((DBObject) unwind.get("$unwind"),
isBsonObject(). //
containing("includeArrayIndex", "x").//
containing("preserveNullAndEmptyArrays", true));
}
/**
* @see DATAMONGO-1391
*/
@Test
public void unwindOperationWithPreserveNullShouldBuildCorrectClause() {
DBObject agg = newAggregation( //
unwind("a", true)).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
DBObject unwind = ((List<DBObject>) agg.get("pipeline")).get(0);
assertThat(unwind,
isBsonObject().notContaining("includeArrayIndex").containing("preserveNullAndEmptyArrays", true));
}
/**
* @see DATAMONGO-753
*/

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
import org.junit.Test;
import org.springframework.data.mongodb.core.DBObjectTestUtils;
import com.mongodb.DBObject;
/**
* Unit tests for {@link UnwindOperation}.
*
* @author Mark Paluch
*/
public class UnwindOperationUnitTests {
/**
* @see DATAMONGO-1391
*/
@Test
public void unwindWithPathOnlyShouldUsePreMongo32Syntax() {
UnwindOperation unwindOperation = Aggregation.unwind("a");
DBObject pipeline = unwindOperation.toDBObject(Aggregation.DEFAULT_CONTEXT);
assertThat(pipeline, isBsonObject().containing("$unwind", "$a"));
}
/**
* @see DATAMONGO-1391
*/
@Test
public void unwindWithArrayIndexShouldUseMongo32Syntax() {
UnwindOperation unwindOperation = Aggregation.unwind("a", "index");
DBObject unwindClause = extractDbObjectFromUnwindOperation(unwindOperation);
assertThat(unwindClause,
isBsonObject().containing("path", "$a").//
containing("preserveNullAndEmptyArrays", false).//
containing("includeArrayIndex", "index"));
}
/**
* @see DATAMONGO-1391
*/
@Test
public void unwindWithArrayIndexShouldExposeArrayIndex() {
UnwindOperation unwindOperation = Aggregation.unwind("a", "index");
assertThat(unwindOperation.getFields().getField("index"), is(not(nullValue())));
}
/**
* @see DATAMONGO-1391
*/
@Test
public void plainUnwindShouldNotExposeIndex() {
UnwindOperation unwindOperation = Aggregation.unwind("a");
assertThat(unwindOperation.getFields().exposesNoFields(), is(true));
}
/**
* @see DATAMONGO-1391
*/
@Test
public void unwindWithPreserveNullShouldUseMongo32Syntax() {
UnwindOperation unwindOperation = Aggregation.unwind("a", true);
DBObject unwindClause = extractDbObjectFromUnwindOperation(unwindOperation);
assertThat(unwindClause,
isBsonObject().containing("path", "$a").//
containing("preserveNullAndEmptyArrays", true).//
notContaining("includeArrayIndex"));
}
/**
* @see DATAMONGO-1391
*/
@Test
public void lookupBuilderBuildsCorrectClause() {
UnwindOperation unwindOperation = UnwindOperation.newUnwind().path("$foo").noArrayIndex().skipNullAndEmptyArrays();
DBObject pipeline = unwindOperation.toDBObject(Aggregation.DEFAULT_CONTEXT);
assertThat(pipeline, isBsonObject().containing("$unwind", "$foo"));
}
/**
* @see DATAMONGO-1391
*/
@Test
public void lookupBuilderBuildsCorrectClauseForMongo32() {
UnwindOperation unwindOperation = UnwindOperation.newUnwind().path("$foo").arrayIndex("myindex")
.preserveNullAndEmptyArrays();
DBObject unwindClause = extractDbObjectFromUnwindOperation(unwindOperation);
assertThat(unwindClause,
isBsonObject().containing("path", "$foo").//
containing("preserveNullAndEmptyArrays", true).//
notContaining("myindex"));
}
private DBObject extractDbObjectFromUnwindOperation(UnwindOperation unwindOperation) {
DBObject dbObject = unwindOperation.toDBObject(Aggregation.DEFAULT_CONTEXT);
DBObject unwindClause = DBObjectTestUtils.getAsDBObject(dbObject, "$unwind");
return unwindClause;
}
}