DATAMONGO-2312 - Add support for array projections.

Original pull request: #770.
This commit is contained in:
Christoph Strobl
2019-07-01 10:46:48 +02:00
committed by Mark Paluch
parent 7c25675cbb
commit c2436bcdfc
2 changed files with 203 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.bson.Document;
@@ -177,6 +178,48 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
return new ProjectionOperation(this.projections, FieldProjection.from(fields, true));
}
/**
* Includes the current {@link ProjectionOperation} as an array with given name. <br />
* If you want to specify array values directly use {@link #andArrayOf(Object...)}.
*
* @param name the target property name.
* @return new instance of {@link ProjectionOperation}.
* @since 2.2
*/
public ProjectionOperation asArray(String name) {
return new ProjectionOperation(Collections.emptyList(),
Collections.singletonList(new ArrayProjection(Fields.field(name), (List) this.projections)));
}
/**
* Includes the given values ({@link Field field references}, {@link AggregationExpression expression}, plain values)
* as an array. <br />
* The target property name needs to be set via {@link ArrayProjectionOperationBuilder#as(String)}.
*
* @param values must not be {@literal null}.
* @return new instance of {@link ArrayProjectionOperationBuilder}.
* @throws IllegalArgumentException if the required argument it {@literal null}.
* @since 2.2
*/
public ArrayProjectionOperationBuilder andArrayOf(Object... values) {
ArrayProjectionOperationBuilder builder = new ArrayProjectionOperationBuilder(this);
for (Object value : values) {
if (value instanceof Field) {
builder.and((Field) value);
} else if (value instanceof AggregationExpression) {
builder.and((AggregationExpression) value);
} else {
builder.and(value);
}
}
return builder;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields()
@@ -1743,4 +1786,95 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
return context.getMappedObject(projections, type);
}
}
/**
* @author Christoph Strobl
* @since 2.2
*/
public static class ArrayProjectionOperationBuilder {
private ProjectionOperation target;
private final List<Object> projections;
public ArrayProjectionOperationBuilder(ProjectionOperation target) {
this.target = target;
this.projections = new ArrayList<>();
}
public ArrayProjectionOperationBuilder and(AggregationExpression expression) {
this.projections.add(expression);
return this;
}
public ArrayProjectionOperationBuilder and(Field field) {
this.projections.add(field);
return this;
}
public ArrayProjectionOperationBuilder and(Object value) {
this.projections.add(value);
return this;
}
/**
* Create the {@link ProjectionOperation} for the array property with given {@literal name}.
*
* @param name The target property name. Must not be {@literal null}.
* @return new instance of {@link ArrayProjectionOperationBuilder}.
*/
public ProjectionOperation as(String name) {
return new ProjectionOperation(target.projections,
Collections.singletonList(new ArrayProjection(Fields.field(name), this.projections)));
}
}
/**
* @author Christoph Strobl
* @since 2.2
*/
static class ArrayProjection extends Projection {
private final Field targetField;
private final List<Object> projections;
public ArrayProjection(Field targetField, List<Object> projections) {
super(targetField);
this.targetField = targetField;
this.projections = projections;
}
@Override
public Document toDocument(AggregationOperationContext context) {
return new Document(targetField.getName(),
projections.stream().map(it -> toArrayEntry(it, context)).collect(Collectors.toList()));
}
private Object toArrayEntry(Object projection, AggregationOperationContext ctx) {
if (projection instanceof Field) {
return ctx.getReference((Field) projection).toString();
}
if (projection instanceof AggregationExpression) {
return ((AggregationExpression) projection).toDocument(ctx);
}
if (projection instanceof FieldProjection) {
return ctx.getReference(((FieldProjection) projection).getExposedField().getTarget()).toString();
}
if (projection instanceof Projection) {
((Projection) projection).toDocument(ctx);
}
return projection;
}
}
}

View File

@@ -29,6 +29,7 @@ import java.util.List;
import org.bson.Document;
import org.junit.Test;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Range.Bound;
import org.springframework.data.mongodb.core.DocumentTestUtils;
@@ -2157,6 +2158,67 @@ public class ProjectionOperationUnitTests {
assertThat(projectClause).isEmpty();
}
@Test // DATAMONGO-2312
public void simpleFieldReferenceAsArray() {
org.bson.Document doc = Aggregation.newAggregation(project("x", "y", "someField").asArray("myArray"))
.toDocument("coll", Aggregation.DEFAULT_CONTEXT);
assertThat(doc).isEqualTo(Document.parse(
"{\"aggregate\":\"coll\", \"pipeline\":[ { $project: { myArray: [ \"$x\", \"$y\", \"$someField\" ] } } ] }"));
}
@Test // DATAMONGO-2312
public void mappedFieldReferenceAsArray() {
MongoMappingContext mappingContext = new MongoMappingContext();
org.bson.Document doc = Aggregation
.newAggregation(BookWithFieldAnnotation.class, project("title", "author").asArray("myArray"))
.toDocument("coll", new TypeBasedAggregationOperationContext(BookWithFieldAnnotation.class, mappingContext,
new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext))));
assertThat(doc).isEqualTo(Document
.parse("{\"aggregate\":\"coll\", \"pipeline\":[ { $project: { myArray: [ \"$ti_t_le\", \"$author\" ] } } ] }"));
}
@Test // DATAMONGO-2312
public void arrayWithNullValue() {
Document doc = project() //
.andArrayOf(Fields.field("field-1"), null, "value").as("myArray") //
.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(doc).isEqualTo(Document.parse("{ $project: { \"myArray\" : [ \"$field-1\", null, \"value\" ] } }"));
}
@Test // DATAMONGO-2312
public void nestedArrayField() {
Document doc = project("_id", "value") //
.andArrayOf(Fields.field("field-1"), "plain - string", ArithmeticOperators.valueOf("field-1").sum().and(10))
.as("myArray") //
.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(doc).isEqualTo(Document.parse(
"{ $project: { \"_id\" : 1, \"value\" : 1, \"myArray\" : [ \"$field-1\", \"plain - string\", { \"$sum\" : [\"$field-1\", 10] } ] } } ] }"));
}
@Test // DATAMONGO-2312
public void nestedMappedFieldReferenceInArrayField() {
MongoMappingContext mappingContext = new MongoMappingContext();
Document doc = project("author") //
.andArrayOf(Fields.field("title"), "plain - string", ArithmeticOperators.valueOf("title").sum().and(10))
.as("myArray") //
.toDocument(new TypeBasedAggregationOperationContext(BookWithFieldAnnotation.class, mappingContext,
new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext))));
assertThat(doc).isEqualTo(Document.parse(
"{ $project: { \"author\" : 1, \"myArray\" : [ \"$ti_t_le\", \"plain - string\", { \"$sum\" : [\"$ti_t_le\", 10] } ] } } ] }"));
}
private static Document exctractOperation(String field, Document fromProjectClause) {
return (Document) fromProjectClause.get(field);
}
@@ -2167,6 +2229,13 @@ public class ProjectionOperationUnitTests {
Author author;
}
@Data
static class BookWithFieldAnnotation {
@Field("ti_t_le") String title;
Author author;
}
@Data
static class BookRenamed {
@Field("ti_tl_e") String title;