Browse Source

DATAMONGO-2041 - Apply field restriction to DTO projections.

We now derive field projections for DTO projections if the field projection document is unrestricted.

Original pull request: #591.
pull/592/merge
Christoph Strobl 7 years ago committed by Mark Paluch
parent
commit
016892085c
  1. 32
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  2. 74
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java
  3. 39
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
  4. 18
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java
  5. 6
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
  6. 6
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java
  7. 6
      src/main/asciidoc/reference/mongodb.adoc

32
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

@ -22,6 +22,7 @@ import lombok.AllArgsConstructor; @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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 <S>
* @param <T>

74
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java

@ -0,0 +1,74 @@ @@ -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<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
PropertyOperations(MappingContext<? extends MongoPersistentEntity<?>, 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;
}
}

39
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; @@ -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 @@ -161,6 +160,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final SpelAwareProxyProjectionFactory projectionFactory;
private final ApplicationListener<MappingContextEvent<?, ?>> 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 @@ -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 @@ -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<Throwable> subscriptionExceptionHandler) {
@ -494,10 +496,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -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 @@ -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) {

18
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java

@ -108,6 +108,18 @@ public class ExecutableFindOperationSupportTests { @@ -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 { @@ -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;

6
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java

@ -917,12 +917,12 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @@ -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

6
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java

@ -311,12 +311,12 @@ public class ReactiveMongoTemplateUnitTests { @@ -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

6
src/main/asciidoc/reference/mongodb.adoc

@ -1745,7 +1745,11 @@ List<Jedi> all = ops.find(SWCharacter.class) <1> @@ -1745,7 +1745,11 @@ List<Jedi> all = ops.find(SWCharacter.class) <1>
<2> Resulting documents are mapped into `Jedi`.
====
TIP: You can directly apply <<projections>> to result documents by providing the `interface` type with `as(Class<?>)`.
TIP: You can directly apply <<projections>> 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()`.

Loading…
Cancel
Save