diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index aa8033781..7665a07a2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -22,6 +22,7 @@ import lombok.AllArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import java.beans.PropertyDescriptor; import java.io.IOException; import java.util.*; import java.util.concurrent.TimeUnit; @@ -178,6 +179,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private final JsonSchemaMapper schemaMapper; private final SpelAwareProxyProjectionFactory projectionFactory; private final EntityOperations operations; + private final PropertyOperations propertyOperations; private @Nullable WriteConcern writeConcern; private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE; @@ -237,6 +239,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter); this.projectionFactory = new SpelAwareProxyProjectionFactory(); this.operations = new EntityOperations(this.mongoConverter.getMappingContext()); + this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext()); // We always have a mapping context in the converter, whether it's a simple one or not mappingContext = this.mongoConverter.getMappingContext(); @@ -263,6 +266,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.projectionFactory = that.projectionFactory; this.mappingContext = that.mappingContext; this.operations = that.operations; + this.propertyOperations = that.propertyOperations; } /** @@ -2724,31 +2728,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } private Document getMappedFieldsObject(Document fields, MongoPersistentEntity entity, Class targetType) { - return queryMapper.getMappedFields(addFieldsForProjection(fields, entity.getType(), targetType), entity); - } - - /** - * For cases where {@code fields} is {@literal null} or {@literal empty} add fields required for creating the - * projection (target) type if the {@code targetType} is a {@literal closed interface projection}. - * - * @param fields can be {@literal null}. - * @param domainType must not be {@literal null}. - * @param targetType must not be {@literal null}. - * @return {@link Document} with fields to be included. - */ - private Document addFieldsForProjection(Document fields, Class domainType, Class targetType) { - - if (!fields.isEmpty() || !targetType.isInterface() || ClassUtils.isAssignable(domainType, targetType)) { - return fields; - } - ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(targetType); + Document projectedFields = propertyOperations.computeFieldsForProjection(projectionFactory, fields, + entity.getType(), targetType); - if (projectionInformation.isClosed()) { - projectionInformation.getInputProperties().forEach(it -> fields.append(it.getName(), 1)); + if (ObjectUtils.nullSafeEquals(fields, projectedFields)) { + return queryMapper.getMappedFields(projectedFields, entity); } - return fields; + return queryMapper.getMappedFields(projectedFields, mappingContext.getPersistentEntity(targetType)); } /** @@ -3014,7 +3002,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, /** * {@link DocumentCallback} transforming {@link Document} into the given {@code targetType} or decorating the - * {@code sourceType} with a {@literal projection} in case the {@code targetType} is an {@litera interface}. + * {@code sourceType} with a {@literal projection} in case the {@code targetType} is an {@literal interface}. * * @param * @param diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java new file mode 100644 index 000000000..57ab2d7fa --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java @@ -0,0 +1,74 @@ +/* + * Copyright 2018 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; + +import org.bson.Document; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.ProjectionInformation; +import org.springframework.util.ClassUtils; + +/** + * Common operations performed on properties of an entity like extracting fields information for projection creation. + * + * @author Christoph Strobl + * @since 2.1 + */ +class PropertyOperations { + + private final MappingContext, MongoPersistentProperty> mappingContext; + + PropertyOperations(MappingContext, MongoPersistentProperty> mappingContext) { + this.mappingContext = mappingContext; + } + + /** + * For cases where {@code fields} is {@literal null} or {@literal empty} add fields required for creating the + * projection (target) type if the {@code targetType} is a {@literal closed interface projection}. + * + * @param projectionFactory must not be {@literal null}. + * @param fields must not be {@literal null}. + * @param domainType must not be {@literal null}. + * @param targetType must not be {@literal null}. + * @return {@link Document} with fields to be included. + */ + Document computeFieldsForProjection(ProjectionFactory projectionFactory, Document fields, Class domainType, + Class targetType) { + + if (!fields.isEmpty() || ClassUtils.isAssignable(domainType, targetType)) { + return fields; + } + + Document projectedFields = new Document(); + + if (targetType.isInterface()) { + + ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(targetType); + + if (projectionInformation.isClosed()) { + projectionInformation.getInputProperties().forEach(it -> projectedFields.append(it.getName(), 1)); + } + } else { + mappingContext.getPersistentEntity(targetType).doWithProperties( + (SimplePropertyHandler) persistentProperty -> projectedFields.append(persistentProperty.getName(), 1)); + } + + return projectedFields; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 97b73e4c4..42d0ba00a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -93,7 +93,6 @@ import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.validation.Validator; -import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; @@ -161,6 +160,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private final SpelAwareProxyProjectionFactory projectionFactory; private final ApplicationListener> indexCreatorListener; private final EntityOperations operations; + private final PropertyOperations propertyOperations; private @Nullable WriteConcern writeConcern; private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE; @@ -226,6 +226,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati // We always have a mapping context in the converter, whether it's a simple one or not this.mappingContext = this.mongoConverter.getMappingContext(); this.operations = new EntityOperations(this.mappingContext); + this.propertyOperations = new PropertyOperations(this.mappingContext); // We create indexes based on mapping events if (this.mappingContext instanceof MongoMappingContext) { @@ -253,6 +254,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.indexCreatorListener = that.indexCreatorListener; this.mappingContext = that.mappingContext; this.operations = that.operations; + this.propertyOperations = that.propertyOperations; } private void onCheckForIndexes(MongoPersistentEntity entity, Consumer subscriptionExceptionHandler) { @@ -494,10 +496,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati session.startTransaction(); } - return Flux.usingWhen(Mono.just(session), // - s -> ReactiveMongoTemplate.this.withSession(action, s), // - ClientSession::commitTransaction, // - ClientSession::abortTransaction) // + return Flux + .usingWhen(Mono.just(session), // + s -> ReactiveMongoTemplate.this.withSession(action, s), // + ClientSession::commitTransaction, // + ClientSession::abortTransaction) // .doFinally(signalType -> doFinally.accept(session)); }); } @@ -2254,31 +2257,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } private Document getMappedFieldsObject(Document fields, MongoPersistentEntity entity, Class targetType) { - return queryMapper.getMappedFields(addFieldsForProjection(fields, entity.getType(), targetType), entity); - } - - /** - * For cases where {@code fields} is {@literal null} or {@literal empty} add fields required for creating the - * projection (target) type if the {@code targetType} is a {@literal closed interface projection}. - * - * @param fields must not be {@literal null}. - * @param domainType must not be {@literal null}. - * @param targetType must not be {@literal null}. - * @return {@link Document} with fields to be included. - */ - private Document addFieldsForProjection(Document fields, Class domainType, Class targetType) { - - if (!fields.isEmpty() || !targetType.isInterface() || ClassUtils.isAssignable(domainType, targetType)) { - return fields; - } - ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(targetType); + Document projectedFields = propertyOperations.computeFieldsForProjection(projectionFactory, fields, + entity.getType(), targetType); - if (projectionInformation.isClosed()) { - projectionInformation.getInputProperties().forEach(it -> fields.append(it.getName(), 1)); + if (ObjectUtils.nullSafeEquals(fields, projectedFields)) { + return queryMapper.getMappedFields(projectedFields, entity); } - return fields; + return queryMapper.getMappedFields(projectedFields, mappingContext.getPersistentEntity(targetType)); } protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java index 93b5c7a0c..f71896479 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java @@ -108,6 +108,18 @@ public class ExecutableFindOperationSupportTests { assertThat(template.query(Person.class).as(Jedi.class).all()).hasOnlyElementsOfType(Jedi.class).hasSize(2); } + @Test // DATAMONGO-2041 + public void findAllWithProjectionOnEmbeddedType() { + + luke.father = new Person(); + luke.father.firstname = "anakin"; + + template.save(luke); + + assertThat(template.query(Person.class).as(PersonDtoProjection.class).matching(query(where("id").is(luke.id))) + .firstValue()).hasFieldOrPropertyWithValue("father", luke.father); + } + @Test // DATAMONGO-1733 public void findByReturningAllValuesAsClosedInterfaceProjection() { @@ -529,6 +541,12 @@ public class ExecutableFindOperationSupportTests { String getName(); } + static class PersonDtoProjection { + + @Field("firstname") String name; + Person father; + } + @Data static class Human { @Id String id; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index b5a5903f6..307e80bb0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -917,12 +917,12 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { verify(findIterable).projection(eq(new Document())); } - @Test // DATAMONGO-1733 - public void doesNotApplyFieldsToDtoProjection() { + @Test // DATAMONGO-1733, DATAMONGO-2041 + public void appliesFieldsToDtoProjection() { template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, null); - verify(findIterable).projection(eq(new Document())); + verify(findIterable).projection(eq(new Document("firstname", 1))); } @Test // DATAMONGO-1733 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index 596182363..dd600e195 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -311,12 +311,12 @@ public class ReactiveMongoTemplateUnitTests { verify(findPublisher, never()).projection(any()); } - @Test // DATAMONGO-1719 - public void doesNotApplyFieldsToDtoProjection() { + @Test // DATAMONGO-1719, DATAMONGO-2041 + public void appliesFieldsToDtoProjection() { template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, null).subscribe(); - verify(findPublisher, never()).projection(any()); + verify(findPublisher).projection(eq(new Document("firstname", 1))); } @Test // DATAMONGO-1719 diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index cb530ce22..4e1aca7b8 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1745,7 +1745,11 @@ List all = ops.find(SWCharacter.class) <1> <2> Resulting documents are mapped into `Jedi`. ==== -TIP: You can directly apply <> to result documents by providing the `interface` type with `as(Class)`. +TIP: You can directly apply <> to result documents by providing the target type via `as(Class)`. + +NOTE: Using projections allows `MongoTemplate` to optimize result mapping by limiting the actual response to fields required +by the projection target type. This applies as long as the `Query` itself does not contain any field restriction and the +target type is a closed interface or DTO projection. You can switch between retrieving a single entity and retrieving multiple entities as a `List` or a `Stream` through the terminating methods: `first()`, `one()`, `all()`, or `stream()`.