DATAMONGO-2200 - Derive fields for aggregation $project stage from a given type.

We now allow to derive field names for a $project stage from a given type by including all top level fields.

    // $project : { title : 1, author : 1 }
    Aggregation.project(Book.class)

Original pull request: #748.
This commit is contained in:
Christoph Strobl
2019-05-15 10:47:35 +02:00
committed by Mark Paluch
parent ff7a189c98
commit 839aa1b1e6
3 changed files with 99 additions and 1 deletions

View File

@@ -244,6 +244,19 @@ public class Aggregation {
return new ProjectionOperation(fields);
}
/**
* Creates a new {@link ProjectionOperation} including all top level fields of the given given {@link Class}.
*
* @param type must not be {@literal null}.
* @return new instance of {@link ProjectionOperation}.
* @since 2.2
*/
public static ProjectionOperation project(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
return new ProjectionOperation(type);
}
/**
* Factory method to create a new {@link UnwindOperation} for the field with the given name.
*

View File

@@ -30,6 +30,7 @@ import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Pro
import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Encapsulates the aggregation framework {@code $project}-operation.
@@ -73,6 +74,16 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
this(NONE, ProjectionOperationBuilder.FieldProjection.from(fields));
}
/**
* Creates a new {@link ProjectionOperation} including all top level fields of the given {@link Class type}.
*
* @param type must not be {@literal null}.
* @since 2.2
*/
public ProjectionOperation(Class<?> type) {
this(NONE, Collections.singletonList(new TypeProjection(type)));
}
/**
* Copy constructor to allow building up {@link ProjectionOperation} instances from already existing
* {@link Projection}s.
@@ -1700,4 +1711,35 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
return new Document(field.getName(), expression.toDocument(context));
}
}
/**
* A {@link Projection} including all top level fields of the given target type mapped to include potentially
* deviating field names.
*
* @since 2.2
* @author Christoph Strobl
*/
static class TypeProjection extends Projection {
private final Class<?> type;
TypeProjection(Class<?> type) {
super(Fields.field(type.getSimpleName()));
this.type = type;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public Document toDocument(AggregationOperationContext context) {
Document projections = new Document();
ReflectionUtils.doWithFields(type, it -> projections.append(it.getName(), 1));
return context.getMappedObject(projections, type);
}
}
}

View File

@@ -27,6 +27,7 @@ import lombok.Data;
import java.util.Arrays;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.bson.Document;
import org.junit.Test;
import org.springframework.data.domain.Range;
@@ -41,6 +42,12 @@ import org.springframework.data.mongodb.core.aggregation.DateOperators.Timezone;
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder;
import org.springframework.data.mongodb.core.aggregation.StringOperators.Concat;
import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
/**
* Unit tests for {@link ProjectionOperation}.
@@ -61,7 +68,7 @@ public class ProjectionOperationUnitTests {
@Test(expected = IllegalArgumentException.class) // DATAMONGO-586
public void rejectsNullFields() {
new ProjectionOperation(null);
new ProjectionOperation((Fields) null);
}
@Test // DATAMONGO-586
@@ -2099,6 +2106,36 @@ public class ProjectionOperationUnitTests {
"{ $project : { newDate: { $dateFromString: { dateString : \"2017-02-08T12:10:40.787\", format : \"dd/mm/yyyy\" } } } }"));
}
@Test // DATAMONGO-2200
public void typeProjectionShouldIncludeTopLevelFieldsOfType() {
ProjectionOperation operation = Aggregation.project(Book.class);
Document document = operation.toDocument(Aggregation.DEFAULT_CONTEXT);
Document projectClause = DocumentTestUtils.getAsDocument(document, PROJECT);
Assertions.assertThat(projectClause) //
.hasSize(2) //
.containsEntry("title", 1) //
.containsEntry("author", 1);
}
@Test // DATAMONGO-2200
public void typeProjectionShouldMapFieldNames() {
MongoMappingContext mappingContext = new MongoMappingContext();
MongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
Document document = Aggregation.project(BookRenamed.class)
.toDocument(new TypeBasedAggregationOperationContext(Book.class, mappingContext, new QueryMapper(converter)));
Document projectClause = DocumentTestUtils.getAsDocument(document, PROJECT);
Assertions.assertThat(projectClause) //
.hasSize(2) //
.containsEntry("ti_tl_e", 1) //
.containsEntry("author", 1);
}
private static Document exctractOperation(String field, Document fromProjectClause) {
return (Document) fromProjectClause.get(field);
}
@@ -2109,6 +2146,12 @@ public class ProjectionOperationUnitTests {
Author author;
}
@Data
static class BookRenamed {
@Field("ti_tl_e") String title;
Author author;
}
@Data
static class Author {
String first;