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.
This commit is contained in:
committed by
Mark Paluch
parent
4f9c0fa6b3
commit
016892085c
@@ -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) {
|
||||
Document projectedFields = propertyOperations.computeFieldsForProjection(projectionFactory, fields,
|
||||
entity.getType(), targetType);
|
||||
|
||||
if (!fields.isEmpty() || !targetType.isInterface() || ClassUtils.isAssignable(domainType, targetType)) {
|
||||
return fields;
|
||||
if (ObjectUtils.nullSafeEquals(fields, projectedFields)) {
|
||||
return queryMapper.getMappedFields(projectedFields, entity);
|
||||
}
|
||||
|
||||
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(targetType);
|
||||
|
||||
if (projectionInformation.isClosed()) {
|
||||
projectionInformation.getInputProperties().forEach(it -> fields.append(it.getName(), 1));
|
||||
}
|
||||
|
||||
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 <S>
|
||||
* @param <T>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<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
|
||||
// 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<Throwable> 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) {
|
||||
Document projectedFields = propertyOperations.computeFieldsForProjection(projectionFactory, fields,
|
||||
entity.getType(), targetType);
|
||||
|
||||
if (!fields.isEmpty() || !targetType.isInterface() || ClassUtils.isAssignable(domainType, targetType)) {
|
||||
return fields;
|
||||
if (ObjectUtils.nullSafeEquals(fields, projectedFields)) {
|
||||
return queryMapper.getMappedFields(projectedFields, entity);
|
||||
}
|
||||
|
||||
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(targetType);
|
||||
|
||||
if (projectionInformation.isClosed()) {
|
||||
projectionInformation.getInputProperties().forEach(it -> fields.append(it.getName(), 1));
|
||||
}
|
||||
|
||||
return fields;
|
||||
return queryMapper.getMappedFields(projectedFields, mappingContext.getPersistentEntity(targetType));
|
||||
}
|
||||
|
||||
protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user