Skip to content

Commit c2436bc

Browse files
christophstroblmp911de
authored andcommitted
DATAMONGO-2312 - Add support for array projections.
Original pull request: #770.
1 parent 7c25675 commit c2436bc

File tree

2 files changed

+203
-0
lines changed

2 files changed

+203
-0
lines changed

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

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Collection;
2121
import java.util.Collections;
2222
import java.util.List;
23+
import java.util.stream.Collectors;
2324

2425
import org.bson.Document;
2526

@@ -177,6 +178,48 @@ public ProjectionOperation andInclude(Fields fields) {
177178
return new ProjectionOperation(this.projections, FieldProjection.from(fields, true));
178179
}
179180

181+
/**
182+
* Includes the current {@link ProjectionOperation} as an array with given name. <br />
183+
* If you want to specify array values directly use {@link #andArrayOf(Object...)}.
184+
*
185+
* @param name the target property name.
186+
* @return new instance of {@link ProjectionOperation}.
187+
* @since 2.2
188+
*/
189+
public ProjectionOperation asArray(String name) {
190+
191+
return new ProjectionOperation(Collections.emptyList(),
192+
Collections.singletonList(new ArrayProjection(Fields.field(name), (List) this.projections)));
193+
}
194+
195+
/**
196+
* Includes the given values ({@link Field field references}, {@link AggregationExpression expression}, plain values)
197+
* as an array. <br />
198+
* The target property name needs to be set via {@link ArrayProjectionOperationBuilder#as(String)}.
199+
*
200+
* @param values must not be {@literal null}.
201+
* @return new instance of {@link ArrayProjectionOperationBuilder}.
202+
* @throws IllegalArgumentException if the required argument it {@literal null}.
203+
* @since 2.2
204+
*/
205+
public ArrayProjectionOperationBuilder andArrayOf(Object... values) {
206+
207+
ArrayProjectionOperationBuilder builder = new ArrayProjectionOperationBuilder(this);
208+
209+
for (Object value : values) {
210+
211+
if (value instanceof Field) {
212+
builder.and((Field) value);
213+
} else if (value instanceof AggregationExpression) {
214+
builder.and((AggregationExpression) value);
215+
} else {
216+
builder.and(value);
217+
}
218+
}
219+
220+
return builder;
221+
}
222+
180223
/*
181224
* (non-Javadoc)
182225
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields()
@@ -1743,4 +1786,95 @@ public Document toDocument(AggregationOperationContext context) {
17431786
return context.getMappedObject(projections, type);
17441787
}
17451788
}
1789+
1790+
/**
1791+
* @author Christoph Strobl
1792+
* @since 2.2
1793+
*/
1794+
public static class ArrayProjectionOperationBuilder {
1795+
1796+
private ProjectionOperation target;
1797+
private final List<Object> projections;
1798+
1799+
public ArrayProjectionOperationBuilder(ProjectionOperation target) {
1800+
1801+
this.target = target;
1802+
this.projections = new ArrayList<>();
1803+
}
1804+
1805+
public ArrayProjectionOperationBuilder and(AggregationExpression expression) {
1806+
1807+
this.projections.add(expression);
1808+
return this;
1809+
}
1810+
1811+
public ArrayProjectionOperationBuilder and(Field field) {
1812+
1813+
this.projections.add(field);
1814+
return this;
1815+
}
1816+
1817+
public ArrayProjectionOperationBuilder and(Object value) {
1818+
1819+
this.projections.add(value);
1820+
return this;
1821+
}
1822+
1823+
/**
1824+
* Create the {@link ProjectionOperation} for the array property with given {@literal name}.
1825+
*
1826+
* @param name The target property name. Must not be {@literal null}.
1827+
* @return new instance of {@link ArrayProjectionOperationBuilder}.
1828+
*/
1829+
public ProjectionOperation as(String name) {
1830+
1831+
return new ProjectionOperation(target.projections,
1832+
Collections.singletonList(new ArrayProjection(Fields.field(name), this.projections)));
1833+
}
1834+
}
1835+
1836+
/**
1837+
* @author Christoph Strobl
1838+
* @since 2.2
1839+
*/
1840+
static class ArrayProjection extends Projection {
1841+
1842+
private final Field targetField;
1843+
private final List<Object> projections;
1844+
1845+
public ArrayProjection(Field targetField, List<Object> projections) {
1846+
1847+
super(targetField);
1848+
this.targetField = targetField;
1849+
this.projections = projections;
1850+
}
1851+
1852+
@Override
1853+
public Document toDocument(AggregationOperationContext context) {
1854+
1855+
return new Document(targetField.getName(),
1856+
projections.stream().map(it -> toArrayEntry(it, context)).collect(Collectors.toList()));
1857+
}
1858+
1859+
private Object toArrayEntry(Object projection, AggregationOperationContext ctx) {
1860+
1861+
if (projection instanceof Field) {
1862+
return ctx.getReference((Field) projection).toString();
1863+
}
1864+
1865+
if (projection instanceof AggregationExpression) {
1866+
return ((AggregationExpression) projection).toDocument(ctx);
1867+
}
1868+
1869+
if (projection instanceof FieldProjection) {
1870+
return ctx.getReference(((FieldProjection) projection).getExposedField().getTarget()).toString();
1871+
}
1872+
1873+
if (projection instanceof Projection) {
1874+
((Projection) projection).toDocument(ctx);
1875+
}
1876+
1877+
return projection;
1878+
}
1879+
}
17461880
}

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import org.bson.Document;
3131
import org.junit.Test;
32+
3233
import org.springframework.data.domain.Range;
3334
import org.springframework.data.domain.Range.Bound;
3435
import org.springframework.data.mongodb.core.DocumentTestUtils;
@@ -2157,6 +2158,67 @@ public void typeProjectionShouldBeEmptyIfNoPropertiesFound() {
21572158
assertThat(projectClause).isEmpty();
21582159
}
21592160

2161+
@Test // DATAMONGO-2312
2162+
public void simpleFieldReferenceAsArray() {
2163+
2164+
org.bson.Document doc = Aggregation.newAggregation(project("x", "y", "someField").asArray("myArray"))
2165+
.toDocument("coll", Aggregation.DEFAULT_CONTEXT);
2166+
2167+
assertThat(doc).isEqualTo(Document.parse(
2168+
"{\"aggregate\":\"coll\", \"pipeline\":[ { $project: { myArray: [ \"$x\", \"$y\", \"$someField\" ] } } ] }"));
2169+
}
2170+
2171+
@Test // DATAMONGO-2312
2172+
public void mappedFieldReferenceAsArray() {
2173+
2174+
MongoMappingContext mappingContext = new MongoMappingContext();
2175+
2176+
org.bson.Document doc = Aggregation
2177+
.newAggregation(BookWithFieldAnnotation.class, project("title", "author").asArray("myArray"))
2178+
.toDocument("coll", new TypeBasedAggregationOperationContext(BookWithFieldAnnotation.class, mappingContext,
2179+
new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext))));
2180+
2181+
assertThat(doc).isEqualTo(Document
2182+
.parse("{\"aggregate\":\"coll\", \"pipeline\":[ { $project: { myArray: [ \"$ti_t_le\", \"$author\" ] } } ] }"));
2183+
}
2184+
2185+
@Test // DATAMONGO-2312
2186+
public void arrayWithNullValue() {
2187+
2188+
Document doc = project() //
2189+
.andArrayOf(Fields.field("field-1"), null, "value").as("myArray") //
2190+
.toDocument(Aggregation.DEFAULT_CONTEXT);
2191+
2192+
assertThat(doc).isEqualTo(Document.parse("{ $project: { \"myArray\" : [ \"$field-1\", null, \"value\" ] } }"));
2193+
}
2194+
2195+
@Test // DATAMONGO-2312
2196+
public void nestedArrayField() {
2197+
2198+
Document doc = project("_id", "value") //
2199+
.andArrayOf(Fields.field("field-1"), "plain - string", ArithmeticOperators.valueOf("field-1").sum().and(10))
2200+
.as("myArray") //
2201+
.toDocument(Aggregation.DEFAULT_CONTEXT);
2202+
2203+
assertThat(doc).isEqualTo(Document.parse(
2204+
"{ $project: { \"_id\" : 1, \"value\" : 1, \"myArray\" : [ \"$field-1\", \"plain - string\", { \"$sum\" : [\"$field-1\", 10] } ] } } ] }"));
2205+
}
2206+
2207+
@Test // DATAMONGO-2312
2208+
public void nestedMappedFieldReferenceInArrayField() {
2209+
2210+
MongoMappingContext mappingContext = new MongoMappingContext();
2211+
2212+
Document doc = project("author") //
2213+
.andArrayOf(Fields.field("title"), "plain - string", ArithmeticOperators.valueOf("title").sum().and(10))
2214+
.as("myArray") //
2215+
.toDocument(new TypeBasedAggregationOperationContext(BookWithFieldAnnotation.class, mappingContext,
2216+
new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext))));
2217+
2218+
assertThat(doc).isEqualTo(Document.parse(
2219+
"{ $project: { \"author\" : 1, \"myArray\" : [ \"$ti_t_le\", \"plain - string\", { \"$sum\" : [\"$ti_t_le\", 10] } ] } } ] }"));
2220+
}
2221+
21602222
private static Document exctractOperation(String field, Document fromProjectClause) {
21612223
return (Document) fromProjectClause.get(field);
21622224
}
@@ -2167,6 +2229,13 @@ static class Book {
21672229
Author author;
21682230
}
21692231

2232+
@Data
2233+
static class BookWithFieldAnnotation {
2234+
2235+
@Field("ti_t_le") String title;
2236+
Author author;
2237+
}
2238+
21702239
@Data
21712240
static class BookRenamed {
21722241
@Field("ti_tl_e") String title;

0 commit comments

Comments
 (0)