Browse Source

Add general support for direct projections.

Closes: #3894
pull/3919/head
Mark Paluch 4 years ago committed by Christoph Strobl
parent
commit
0070b12f95
No known key found for this signature in database
GPG Key ID: 8CC1AB53391458C8
  1. 23
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
  2. 94
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  3. 45
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java
  4. 67
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java
  5. 105
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
  6. 10
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java
  7. 293
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java
  8. 34
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java
  9. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
  10. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java
  11. 7
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
  12. 92
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/PersistentPropertyTranslator.java
  13. 6
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java
  14. 6
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java
  15. 6
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java
  16. 4
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java
  17. 6
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java
  18. 5
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java
  19. 2
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java
  20. 19
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoOperationsUnitTests.java
  21. 2
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
  22. 3
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java
  23. 4
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java
  24. 19
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverterUnitTests.java
  25. 138
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
  26. 5
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java

23
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java

@ -23,13 +23,17 @@ import java.util.Optional; @@ -23,13 +23,17 @@ import java.util.Optional;
import org.bson.Document;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.EntityProjection;
import org.springframework.data.mapping.context.EntityProjectionIntrospector;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@ -39,6 +43,7 @@ import org.springframework.data.mongodb.core.query.Collation; @@ -39,6 +43,7 @@ import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@ -63,8 +68,19 @@ class EntityOperations { @@ -63,8 +68,19 @@ class EntityOperations {
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context;
EntityOperations(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context) {
private final EntityProjectionIntrospector introspector;
EntityOperations(MongoConverter converter) {
this(converter.getMappingContext(), converter.getCustomConversions(), converter.getProjectionFactory());
}
EntityOperations(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context,
CustomConversions conversions, ProjectionFactory projectionFactory) {
this.context = context;
this.introspector = EntityProjectionIntrospector.create(projectionFactory,
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
.and(((target, underlyingType) -> !conversions.isSimpleType(target))),
context);
}
/**
@ -229,6 +245,11 @@ class EntityOperations { @@ -229,6 +245,11 @@ class EntityOperations {
return UntypedOperations.instance();
}
public <M, D> EntityProjection<M, D> introspectProjection(Class<M> resultType,
Class<D> entityType) {
return introspector.introspect(resultType, entityType);
}
/**
* A representation of information about an entity.
*

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

@ -49,6 +49,7 @@ import org.springframework.data.geo.GeoResults; @@ -49,6 +49,7 @@ import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metric;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.EntityProjection;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoDatabaseUtils;
@ -102,7 +103,6 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; @@ -102,7 +103,6 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.CloseableIterator;
import org.springframework.data.util.Optionals;
import org.springframework.lang.Nullable;
@ -173,7 +173,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -173,7 +173,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
private final QueryMapper queryMapper;
private final UpdateMapper updateMapper;
private final JsonSchemaMapper schemaMapper;
private final SpelAwareProxyProjectionFactory projectionFactory;
private final EntityOperations operations;
private final PropertyOperations propertyOperations;
private final QueryOperations queryOperations;
@ -225,8 +224,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -225,8 +224,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
this.queryMapper = new QueryMapper(this.mongoConverter);
this.updateMapper = new UpdateMapper(this.mongoConverter);
this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter);
this.projectionFactory = new SpelAwareProxyProjectionFactory();
this.operations = new EntityOperations(this.mongoConverter.getMappingContext());
this.operations = new EntityOperations(this.mongoConverter);
this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext());
this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations,
mongoDbFactory);
@ -264,7 +262,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -264,7 +262,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
this.queryMapper = that.queryMapper;
this.updateMapper = that.updateMapper;
this.schemaMapper = that.schemaMapper;
this.projectionFactory = that.projectionFactory;
this.mappingContext = that.mappingContext;
this.operations = that.operations;
this.propertyOperations = that.propertyOperations;
@ -330,9 +327,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -330,9 +327,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
resourceLoader = applicationContext;
projectionFactory.setBeanFactory(applicationContext);
projectionFactory.setBeanClassLoader(applicationContext.getClassLoader());
}
/**
@ -416,15 +410,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -416,15 +410,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
MongoPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(entityType);
QueryContext queryContext = queryOperations.createQueryContext(query);
EntityProjection<T, ?> projection = operations.introspectProjection(returnType,
entityType);
Document mappedQuery = queryContext.getMappedQuery(persistentEntity);
Document mappedFields = queryContext.getMappedFields(persistentEntity, returnType, projectionFactory);
Document mappedFields = queryContext.getMappedFields(persistentEntity, projection);
FindIterable<Document> cursor = new QueryCursorPreparer(query, entityType).initiateFind(collection,
col -> col.find(mappedQuery, Document.class).projection(mappedFields));
return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator,
new ProjectingReadCallback<>(mongoConverter, entityType, returnType, collectionName));
new ProjectingReadCallback<>(mongoConverter, projection, collectionName));
});
}
@ -964,9 +960,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -964,9 +960,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
.withOptions(AggregationOptions.builder().collation(near.getCollation()).build());
AggregationResults<Document> results = aggregate($geoNear, collection, Document.class);
EntityProjection<T, ?> projection = operations.introspectProjection(returnType,
domainType);
DocumentCallback<GeoResult<T>> callback = new GeoNearResultDocumentCallback<>(distanceField,
new ProjectingReadCallback<>(mongoConverter, domainType, returnType, collection), near.getMetric());
new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric());
List<GeoResult<T>> result = new ArrayList<>();
@ -1050,8 +1048,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -1050,8 +1048,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityType);
QueryContext queryContext = queryOperations.createQueryContext(query);
EntityProjection<T, S> projection = operations.introspectProjection(resultType,
entityType);
Document mappedQuery = queryContext.getMappedQuery(entity);
Document mappedFields = queryContext.getMappedFields(entity, resultType, projectionFactory);
Document mappedFields = queryContext.getMappedFields(entity, projection);
Document mappedSort = queryContext.getMappedSort(entity);
replacement = maybeCallBeforeConvert(replacement, collectionName);
@ -1061,7 +1061,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -1061,7 +1061,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
maybeCallBeforeSave(replacement, mappedReplacement, collectionName);
T saved = doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort,
queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, resultType);
queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options,
projection);
if (saved != null) {
maybeEmitEvent(new AfterSaveEvent<>(saved, mappedReplacement, collectionName));
@ -2499,7 +2500,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -2499,7 +2500,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields));
Document mappedFields = queryContext.getMappedFields(entity, entityClass, projectionFactory);
Document mappedFields = queryContext.getMappedFields(entity,
EntityProjection.nonProjecting(entityClass));
Document mappedQuery = queryContext.getMappedQuery(entity);
if (LOGGER.isDebugEnabled()) {
@ -2551,7 +2553,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -2551,7 +2553,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields));
Document mappedFields = queryContext.getMappedFields(entity, entityClass, projectionFactory);
Document mappedFields = queryContext.getMappedFields(entity,
EntityProjection.nonProjecting(entityClass));
Document mappedQuery = queryContext.getMappedQuery(entity);
if (LOGGER.isDebugEnabled()) {
@ -2573,9 +2576,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -2573,9 +2576,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Class<T> targetClass, CursorPreparer preparer) {
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(sourceClass);
EntityProjection<T, S> projection = operations.introspectProjection(targetClass,
sourceClass);
QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields));
Document mappedFields = queryContext.getMappedFields(entity, targetClass, projectionFactory);
Document mappedFields = queryContext.getMappedFields(entity, projection);
Document mappedQuery = queryContext.getMappedQuery(entity);
if (LOGGER.isDebugEnabled()) {
@ -2584,9 +2589,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -2584,9 +2589,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields, null), preparer,
new ProjectingReadCallback<>(mongoConverter, sourceClass, targetClass, collectionName), collectionName);
new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName);
}
/**
* Convert given {@link CollectionOptions} to a document and take the domain type information into account when
* creating a mapped schema for validation. <br />
@ -2745,6 +2751,35 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -2745,6 +2751,35 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class<?> entityType,
Document replacement, FindAndReplaceOptions options, Class<T> resultType) {
EntityProjection<T, ?> projection = operations.introspectProjection(resultType,
entityType);
return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement,
options, projection);
}
/**
* Customize this part for findAndReplace.
*
* @param collectionName The name of the collection to perform the operation in.
* @param mappedQuery the query to look up documents.
* @param mappedFields the fields to project the result to.
* @param mappedSort the sort to be applied when executing the query.
* @param collation collation settings for the query. Can be {@literal null}.
* @param entityType the source domain type.
* @param replacement the replacement {@link Document}.
* @param options applicable options.
* @param projection the projection descriptor.
* @return {@literal null} if object does not exist, {@link FindAndReplaceOptions#isReturnNew() return new} is
* {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}.
* @since 3.4
*/
@Nullable
private <T> T doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields,
Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class<?> entityType,
Document replacement, FindAndReplaceOptions options,
EntityProjection<T, ?> projection) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format(
"findAndReplace using query: %s fields: %s sort: %s for class: %s and replacement: %s " + "in collection: %s",
@ -2754,7 +2789,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -2754,7 +2789,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return executeFindOneInternal(
new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options),
new ProjectingReadCallback<>(mongoConverter, entityType, resultType, collectionName), collectionName);
new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName);
}
/**
@ -3205,17 +3240,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -3205,17 +3240,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*/
private class ProjectingReadCallback<S, T> implements DocumentCallback<T> {
private final EntityReader<Object, Bson> reader;
private final Class<S> entityType;
private final Class<T> targetType;
private final MongoConverter reader;
private final EntityProjection<T, S> projection;
private final String collectionName;
ProjectingReadCallback(EntityReader<Object, Bson> reader, Class<S> entityType, Class<T> targetType,
ProjectingReadCallback(MongoConverter reader, EntityProjection<T, S> projection,
String collectionName) {
this.reader = reader;
this.entityType = entityType;
this.targetType = targetType;
this.projection = projection;
this.collectionName = collectionName;
}
@ -3230,21 +3263,16 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -3230,21 +3263,16 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return null;
}
Class<?> typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType
: targetType;
maybeEmitEvent(new AfterLoadEvent<>(document, projection.getMappedType().getType(), collectionName));
maybeEmitEvent(new AfterLoadEvent<>(document, targetType, collectionName));
Object entity = reader.read(typeToRead, document);
Object entity = reader.project(projection, document);
if (entity == null) {
throw new MappingException(String.format("EntityReader %s returned null", reader));
}
Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, entity) : entity;
maybeEmitEvent(new AfterConvertEvent<>(document, result, collectionName));
return (T) maybeCallAfterConvert(result, document, collectionName);
maybeEmitEvent(new AfterConvertEvent<>(document, entity, collectionName));
return (T) maybeCallAfterConvert(entity, document, collectionName);
}
}

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

@ -16,18 +16,19 @@ @@ -16,18 +16,19 @@
package org.springframework.data.mongodb.core;
import org.bson.Document;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.context.EntityProjection;
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;
import org.springframework.data.mongodb.core.mapping.PersistentPropertyTranslator;
import org.springframework.data.util.Predicates;
/**
* Common operations performed on properties of an entity like extracting fields information for projection creation.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
class PropertyOperations {
@ -40,37 +41,37 @@ class PropertyOperations { @@ -40,37 +41,37 @@ class PropertyOperations {
/**
* For cases where {@code fields} is {@link Document#isEmpty() empty} include only fields that are required for
* creating the projection (target) type if the {@code targetType} is a {@literal DTO projection} or a
* creating the projection (target) type if the {@code EntityProjection} is a {@literal DTO projection} or a
* {@literal closed interface projection}.
*
* @param projectionFactory must not be {@literal null}.
* @param projection 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) {
Document computeMappedFieldsForProjection(EntityProjection<?, ?> projection,
Document fields) {
if (!fields.isEmpty() || ClassUtils.isAssignable(domainType, targetType)) {
if (!projection.isProjection() || !projection.isClosedProjection()) {
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));
}
if (projection.getMappedType().getType().isInterface()) {
projection.forEach(it -> {
projectedFields.put(it.getPropertyPath().getSegment(), 1);
});
} else {
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(targetType);
if (entity != null) {
entity.doWithProperties(
(SimplePropertyHandler) persistentProperty -> projectedFields.append(persistentProperty.getName(), 1));
// DTO projections use merged metadata between domain type and result type
PersistentPropertyTranslator translator = PersistentPropertyTranslator.create(
mappingContext.getRequiredPersistentEntity(projection.getDomainType()),
Predicates.negate(MongoPersistentProperty::hasExplicitFieldName));
MongoPersistentEntity<?> persistentEntity = mappingContext
.getRequiredPersistentEntity(projection.getMappedType());
for (MongoPersistentProperty property : persistentEntity) {
projectedFields.put(translator.translate(property).getFieldName(), 1);
}
}

67
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java

@ -28,8 +28,10 @@ import java.util.stream.Collectors; @@ -28,8 +28,10 @@ import java.util.stream.Collectors;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.codecs.Codec;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.mapping.context.EntityProjection;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.CodecRegistryProvider;
import org.springframework.data.mongodb.MongoExpression;
@ -54,11 +56,9 @@ import org.springframework.data.mongodb.core.query.Query; @@ -54,11 +56,9 @@ import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import com.mongodb.client.model.CountOptions;
@ -288,45 +288,58 @@ class QueryOperations { @@ -288,45 +288,58 @@ class QueryOperations {
return queryMapper.getMappedObject(getQueryObject(), entity);
}
Document getMappedFields(@Nullable MongoPersistentEntity<?> entity, Class<?> targetType,
ProjectionFactory projectionFactory) {
Document fields = new Document();
Document getMappedFields(@Nullable MongoPersistentEntity<?> entity,
EntityProjection<?, ?> projection) {
for (Entry<String, Object> entry : query.getFieldsObject().entrySet()) {
if (entry.getValue() instanceof MongoExpression) {
Document fields = evaluateFields(entity);
AggregationOperationContext ctx = entity == null ? Aggregation.DEFAULT_CONTEXT
: new RelaxedTypeBasedAggregationOperationContext(entity.getType(), mappingContext, queryMapper);
if (entity == null) {
return fields;
}
fields.put(entry.getKey(), AggregationExpression.from((MongoExpression) entry.getValue()).toDocument(ctx));
Document mappedFields;
if (!fields.isEmpty()) {
mappedFields = queryMapper.getMappedFields(fields, entity);
} else {
fields.put(entry.getKey(), entry.getValue());
mappedFields = propertyOperations.computeMappedFieldsForProjection(projection, fields);
}
if (entity.hasTextScoreProperty() && mappedFields.containsKey(entity.getTextScoreProperty().getFieldName())
&& !query.getQueryObject().containsKey("$text")) {
mappedFields.remove(entity.getTextScoreProperty().getFieldName());
}
Document mappedFields = fields;
if (mappedFields.isEmpty()) {
return BsonUtils.EMPTY_DOCUMENT;
}
if (entity == null) {
return mappedFields;
}
Document projectedFields = propertyOperations.computeFieldsForProjection(projectionFactory, fields,
entity.getType(), targetType);
private Document evaluateFields(@Nullable MongoPersistentEntity<?> entity) {
if (ObjectUtils.nullSafeEquals(fields, projectedFields)) {
mappedFields = queryMapper.getMappedFields(projectedFields, entity);
} else {
mappedFields = queryMapper.getMappedFields(projectedFields,
mappingContext.getRequiredPersistentEntity(targetType));
Document fields = query.getFieldsObject();
if (fields.isEmpty()) {
return BsonUtils.EMPTY_DOCUMENT;
}
if (entity.hasTextScoreProperty() && !query.getQueryObject().containsKey("$text")) {
mappedFields.remove(entity.getTextScoreProperty().getFieldName());
Document evaluated = new Document();
for (Entry<String, Object> entry : fields.entrySet()) {
if (entry.getValue() instanceof MongoExpression) {
AggregationOperationContext ctx = entity == null ? Aggregation.DEFAULT_CONTEXT
: new RelaxedTypeBasedAggregationOperationContext(entity.getType(), mappingContext, queryMapper);
evaluated.put(entry.getKey(), AggregationExpression.from((MongoExpression) entry.getValue()).toDocument(ctx));
} else {
evaluated.put(entry.getKey(), entry.getValue());
}
}
return mappedFields;
return evaluated;
}
/**
@ -388,8 +401,8 @@ class QueryOperations { @@ -388,8 +401,8 @@ class QueryOperations {
}
@Override
Document getMappedFields(@Nullable MongoPersistentEntity<?> entity, Class<?> targetType,
ProjectionFactory projectionFactory) {
Document getMappedFields(@Nullable MongoPersistentEntity<?> entity,
EntityProjection<?, ?> projection) {
return getMappedFields(entity);
}

105
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java

@ -63,6 +63,7 @@ import org.springframework.data.geo.Metric; @@ -63,6 +63,7 @@ import org.springframework.data.geo.Metric;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
import org.springframework.data.mapping.context.EntityProjection;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.MappingContextEvent;
import org.springframework.data.mongodb.MongoDatabaseFactory;
@ -113,7 +114,6 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; @@ -113,7 +114,6 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.Optionals;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -175,7 +175,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -175,7 +175,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final QueryMapper queryMapper;
private final UpdateMapper updateMapper;
private final JsonSchemaMapper schemaMapper;
private final SpelAwareProxyProjectionFactory projectionFactory;
private final ApplicationListener<MappingContextEvent<?, ?>> indexCreatorListener;
private final EntityOperations operations;
private final PropertyOperations propertyOperations;
@ -242,13 +241,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -242,13 +241,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.queryMapper = new QueryMapper(this.mongoConverter);
this.updateMapper = new UpdateMapper(this.mongoConverter);
this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter);
this.projectionFactory = new SpelAwareProxyProjectionFactory();
this.indexCreatorListener = new IndexCreatorEventListener(subscriptionExceptionHandler);
// 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);
this.operations = new EntityOperations(this.mongoConverter);
this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext());
this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations,
mongoDatabaseFactory);
@ -276,7 +274,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -276,7 +274,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.queryMapper = that.queryMapper;
this.updateMapper = that.updateMapper;
this.schemaMapper = that.schemaMapper;
this.projectionFactory = that.projectionFactory;
this.indexCreator = that.indexCreator;
this.indexCreatorListener = that.indexCreatorListener;
this.mappingContext = that.mappingContext;
@ -353,9 +350,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -353,9 +350,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
if (mappingContext instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher);
}
projectionFactory.setBeanFactory(applicationContext);
projectionFactory.setBeanClassLoader(applicationContext.getClassLoader());
}
/**
@ -1058,9 +1052,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -1058,9 +1052,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
String collection = StringUtils.hasText(collectionName) ? collectionName : getCollectionName(entityClass);
String distanceField = operations.nearQueryDistanceFieldName(entityClass);
EntityProjection<T, ?> projection = operations.introspectProjection(returnType,
entityClass);
GeoNearResultDocumentCallback<T> callback = new GeoNearResultDocumentCallback<>(distanceField,
new ProjectingReadCallback<>(mongoConverter, entityClass, returnType, collection), near.getMetric());
new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric());
Aggregation $geoNear = TypedAggregation.newAggregation(entityClass, Aggregation.geoNear(near, distanceField))
.withOptions(AggregationOptions.builder().collation(near.getCollation()).build());
@ -1139,9 +1135,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -1139,9 +1135,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityType);
QueryContext queryContext = queryOperations.createQueryContext(query);
EntityProjection<T, S> projection = operations.introspectProjection(resultType,
entityType);
Document mappedQuery = queryContext.getMappedQuery(entity);
Document mappedFields = queryContext.getMappedFields(entity, resultType, projectionFactory);
Document mappedFields = queryContext.getMappedFields(entity, projection);
Document mappedSort = queryContext.getMappedSort(entity);
return Mono.defer(() -> {
@ -1161,7 +1159,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -1161,7 +1159,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}).flatMap(it -> {
Mono<T> afterFindAndReplace = doFindAndReplace(it.getCollection(), mappedQuery, mappedFields, mappedSort,
queryContext.getCollation(entityType).orElse(null), entityType, it.getTarget(), options, resultType);
queryContext.getCollation(entityType).orElse(null), entityType, it.getTarget(), options,
projection);
return afterFindAndReplace.flatMap(saved -> {
maybeEmitEvent(new AfterSaveEvent<>(saved, it.getTarget(), it.getCollection()));
return maybeCallAfterSave(saved, it.getTarget(), it.getCollection());
@ -2373,7 +2372,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2373,7 +2372,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
QueryContext queryContext = queryOperations
.createQueryContext(new BasicQuery(query, fields != null ? fields : new Document()));
Document mappedFields = queryContext.getMappedFields(entity, entityClass, projectionFactory);
Document mappedFields = queryContext.getMappedFields(entity,
EntityProjection.nonProjecting(entityClass));
Document mappedQuery = queryContext.getMappedQuery(entity);
if (LOGGER.isDebugEnabled()) {
@ -2425,7 +2425,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2425,7 +2425,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields));
Document mappedFields = queryContext.getMappedFields(entity, entityClass, projectionFactory);
Document mappedFields = queryContext.getMappedFields(entity,
EntityProjection.nonProjecting(entityClass));
Document mappedQuery = queryContext.getMappedQuery(entity);
if (LOGGER.isDebugEnabled()) {
@ -2447,9 +2448,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2447,9 +2448,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Class<T> targetClass, FindPublisherPreparer preparer) {
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(sourceClass);
EntityProjection<T, S> projection = operations.introspectProjection(targetClass,
sourceClass);
QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields));
Document mappedFields = queryContext.getMappedFields(entity, targetClass, projectionFactory);
Document mappedFields = queryContext.getMappedFields(entity, projection);
Document mappedQuery = queryContext.getMappedQuery(entity);
if (LOGGER.isDebugEnabled()) {
@ -2458,24 +2461,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2458,24 +2461,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields), preparer,
new ProjectingReadCallback<>(mongoConverter, sourceClass, targetClass, collectionName), collectionName);
}
private Document getMappedFieldsObject(Document fields, @Nullable MongoPersistentEntity<?> entity,
Class<?> targetType) {
if (entity == null) {
return fields;
}
Document projectedFields = propertyOperations.computeFieldsForProjection(projectionFactory, fields,
entity.getType(), targetType);
if (ObjectUtils.nullSafeEquals(fields, projectedFields)) {
return queryMapper.getMappedFields(projectedFields, entity);
}
return queryMapper.getMappedFields(projectedFields, mappingContext.getRequiredPersistentEntity(targetType));
new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName);
}
protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions) {
@ -2610,6 +2596,34 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2610,6 +2596,34 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Document mappedSort, com.mongodb.client.model.Collation collation, Class<?> entityType, Document replacement,
FindAndReplaceOptions options, Class<T> resultType) {
EntityProjection<T, ?> projection = operations.introspectProjection(resultType,
entityType);
return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement,
options, projection);
}
/**
* Customize this part for findAndReplace.
*
* @param collectionName The name of the collection to perform the operation in.
* @param mappedQuery the query to look up documents.
* @param mappedFields the fields to project the result to.
* @param mappedSort the sort to be applied when executing the query.
* @param collation collation settings for the query. Can be {@literal null}.
* @param entityType the source domain type.
* @param replacement the replacement {@link Document}.
* @param options applicable options.
* @param projection the projection descriptor.
* @return {@link Mono#empty()} if object does not exist, {@link FindAndReplaceOptions#isReturnNew() return new} is
* {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}.
* @since 3.4
*/
private <T> Mono<T> doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields,
Document mappedSort, com.mongodb.client.model.Collation collation, Class<?> entityType, Document replacement,
FindAndReplaceOptions options,
EntityProjection<T, ?> projection) {
return Mono.defer(() -> {
if (LOGGER.isDebugEnabled()) {
@ -2622,7 +2636,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2622,7 +2636,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return executeFindOneInternal(
new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options),
new ProjectingReadCallback<>(this.mongoConverter, entityType, resultType, collectionName), collectionName);
new ProjectingReadCallback<>(this.mongoConverter, projection, collectionName), collectionName);
});
}
@ -3203,37 +3217,30 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -3203,37 +3217,30 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
*/
private class ProjectingReadCallback<S, T> implements DocumentCallback<T> {
private final EntityReader<Object, Bson> reader;
private final Class<S> entityType;
private final Class<T> targetType;
private final MongoConverter reader;
private final EntityProjection<T, S> projection;
private final String collectionName;
ProjectingReadCallback(EntityReader<Object, Bson> reader, Class<S> entityType, Class<T> targetType,
ProjectingReadCallback(MongoConverter reader, EntityProjection<T, S> projection,
String collectionName) {
this.reader = reader;
this.entityType = entityType;
this.targetType = targetType;
this.projection = projection;
this.collectionName = collectionName;
}
@SuppressWarnings("unchecked")
public Mono<T> doWith(Document document) {
Class<?> typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) //
? entityType //
: targetType;
Class<T> returnType = projection.getMappedType().getType();
maybeEmitEvent(new AfterLoadEvent<>(document, returnType, collectionName));
maybeEmitEvent(new AfterLoadEvent<>(document, typeToRead, collectionName));
Object entity = reader.read(typeToRead, document);
Object entity = reader.project(projection, document);
if (entity == null) {
throw new MappingException(String.format("EntityReader %s returned null", reader));
}
Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, entity) : entity;
T castEntity = (T) result;
T castEntity = (T) entity;
maybeEmitEvent(new AfterConvertEvent<>(document, castEntity, collectionName));
return maybeCallAfterConvert(castEntity, document, collectionName);
}

10
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java

@ -91,7 +91,7 @@ class DocumentAccessor { @@ -91,7 +91,7 @@ class DocumentAccessor {
public void put(MongoPersistentProperty prop, @Nullable Object value) {
Assert.notNull(prop, "MongoPersistentProperty must not be null!");
String fieldName = prop.getFieldName();
String fieldName = getFieldName(prop);
if (!fieldName.contains(".")) {
BsonUtils.addToMap(document, fieldName, value);
@ -123,7 +123,7 @@ class DocumentAccessor { @@ -123,7 +123,7 @@ class DocumentAccessor {
*/
@Nullable
public Object get(MongoPersistentProperty property) {
return BsonUtils.resolveValue(document, property.getFieldName());
return BsonUtils.resolveValue(document, getFieldName(property));
}
/**
@ -150,7 +150,11 @@ class DocumentAccessor { @@ -150,7 +150,11 @@ class DocumentAccessor {
Assert.notNull(property, "Property must not be null!");
return BsonUtils.hasValue(document, property.getFieldName());
return BsonUtils.hasValue(document, getFieldName(property));
}
String getFieldName(MongoPersistentProperty prop) {
return prop.getFieldName();
}
/**

293
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java

@ -27,6 +27,7 @@ import java.util.List; @@ -27,6 +27,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
@ -46,13 +47,20 @@ import org.springframework.core.CollectionFactory; @@ -46,13 +47,20 @@ import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.annotation.Reference;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.TypeMapper;
import org.springframework.data.mapping.AccessOptions;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PersistentPropertyPathAccessor;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.EntityProjection;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
@ -69,6 +77,7 @@ import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentPropert @@ -69,6 +77,7 @@ import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentPropert
import org.springframework.data.mongodb.core.mapping.DocumentPointer;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.PersistentPropertyTranslator;
import org.springframework.data.mongodb.core.mapping.Unwrapped;
import org.springframework.data.mongodb.core.mapping.Unwrapped.OnEmpty;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
@ -76,7 +85,10 @@ import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent; @@ -76,7 +85,10 @@ import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent;
import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Predicates;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -129,6 +141,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -129,6 +141,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private SpELContext spELContext;
private @Nullable EntityCallbacks entityCallbacks;
private final DocumentPointerFactory documentPointerFactory;
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
/**
* Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}.
@ -212,6 +225,16 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -212,6 +225,16 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return this.typeMapper == null ? this.defaultTypeMapper : this.typeMapper;
}
@Override
public ProjectionFactory getProjectionFactory() {
return projectionFactory;
}
@Override
public CustomConversions getCustomConversions() {
return conversions;
}
/**
* Configure the characters dots potentially contained in a {@link Map} shall be replaced with. By default we don't do
* any translation but rather reject a {@link Map} with keys containing dots causing the conversion for the entire
@ -254,6 +277,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -254,6 +277,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
this.applicationContext = applicationContext;
this.spELContext = new SpELContext(this.spELContext, applicationContext);
this.projectionFactory.setBeanFactory(applicationContext);
this.projectionFactory.setBeanClassLoader(applicationContext.getClassLoader());
if (entityCallbacks == null) {
setEntityCallbacks(EntityCallbacks.create(applicationContext));
@ -281,6 +306,150 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -281,6 +306,150 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
this.entityCallbacks = entityCallbacks;
}
@Override
public <R> R project(EntityProjection<R, ?> projection, Bson bson) {
if (!projection.isProjection()) { // backed by real object
TypeInformation<?> typeToRead = projection.getMappedType().getType().isInterface() ? projection.getDomainType()
: projection.getMappedType();
return (R) read(typeToRead, bson);
}
ProjectingConversionContext context = new ProjectingConversionContext(conversions, ObjectPath.ROOT,
this::readCollectionOrArray, this::readMap, this::readDBRef, this::getPotentiallyConvertedSimpleRead,
projection);
return doReadProjection(context, bson, projection);
}
@SuppressWarnings("unchecked")
private <R> R doReadProjection(ConversionContext context, Bson bson,
EntityProjection<R, ?> projection) {
MongoPersistentEntity<?> entity = getMappingContext().getRequiredPersistentEntity(projection.getActualDomainType());
TypeInformation<?> mappedType = projection.getActualMappedType();
MongoPersistentEntity<R> mappedEntity = (MongoPersistentEntity<R>) getMappingContext()
.getPersistentEntity(mappedType);
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext);
boolean isInterfaceProjection = mappedType.getType().isInterface();
if (isInterfaceProjection) {
PersistentPropertyTranslator propertyTranslator = PersistentPropertyTranslator.create(mappedEntity);
DocumentAccessor documentAccessor = new DocumentAccessor(bson);
PersistentPropertyAccessor<?> accessor = new MapPersistentPropertyAccessor();
PersistentPropertyAccessor<?> convertingAccessor = PropertyTranslatingPropertyAccessor
.create(new ConvertingPropertyAccessor<>(accessor, conversionService), propertyTranslator);
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor,
evaluator);
readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator,
Predicates.isTrue());
return (R) projectionFactory.createProjection(mappedType.getType(), accessor.getBean());
}
// DTO projection
if (mappedEntity == null) {
throw new MappingException(String.format("No mapping metadata found for %s", mappedType.getType().getName()));
}
// create target instance, merge metadata from underlying DTO type
PersistentPropertyTranslator propertyTranslator = PersistentPropertyTranslator.create(entity,
Predicates.negate(MongoPersistentProperty::hasExplicitFieldName));
DocumentAccessor documentAccessor = new DocumentAccessor(bson) {
@Override
String getFieldName(MongoPersistentProperty prop) {
return propertyTranslator.translate(prop).getFieldName();
}
};
PreferredConstructor<?, MongoPersistentProperty> persistenceConstructor = mappedEntity.getPersistenceConstructor();
ParameterValueProvider<MongoPersistentProperty> provider = persistenceConstructor != null
&& persistenceConstructor.hasParameters()
? getParameterProvider(context, mappedEntity, documentAccessor, evaluator)
: NoOpParameterValueProvider.INSTANCE;
EntityInstantiator instantiator = instantiators.getInstantiatorFor(mappedEntity);
R instance = instantiator.createInstance(mappedEntity, provider);
PersistentPropertyAccessor<R> accessor = mappedEntity.getPropertyAccessor(instance);
populateProperties(context, mappedEntity, documentAccessor, evaluator, instance);
PersistentPropertyAccessor<?> convertingAccessor = new ConvertingPropertyAccessor<>(accessor, conversionService);
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, evaluator);
readProperties(context, mappedEntity, convertingAccessor, documentAccessor, valueProvider, evaluator,
Predicates.isTrue());
return accessor.getBean();
}
private Object doReadOrProject(ConversionContext context, Bson source, TypeInformation<?> typeHint,
EntityProjection<?, ?> typeDescriptor) {
if (typeDescriptor.isProjection()) {
return doReadProjection(context, BsonUtils.asDocument(source), typeDescriptor);
}
return readDocument(context, source, typeHint);
}
class ProjectingConversionContext extends ConversionContext {
private final EntityProjection<?, ?> returnedTypeDescriptor;
ProjectingConversionContext(CustomConversions customConversions, ObjectPath path,
ContainerValueConverter<Collection<?>> collectionConverter, ContainerValueConverter<Bson> mapConverter,
ContainerValueConverter<DBRef> dbRefConverter, ValueConverter<Object> elementConverter,
EntityProjection<?, ?> projection) {
super(customConversions, path,
(context, source, typeHint) -> doReadOrProject(context, source, typeHint, projection),
collectionConverter, mapConverter, dbRefConverter, elementConverter);
this.returnedTypeDescriptor = projection;
}
@Override
public ConversionContext forProperty(String name) {
EntityProjection<?, ?> property = returnedTypeDescriptor.findProperty(name);
if (property == null) {
return super.forProperty(name);
}
return new ProjectingConversionContext(conversions, path, collectionConverter, mapConverter, dbRefConverter,
elementConverter, property);
}
@Override
public ConversionContext withPath(ObjectPath currentPath) {
return new ProjectingConversionContext(conversions, currentPath, collectionConverter, mapConverter,
dbRefConverter, elementConverter, returnedTypeDescriptor);
}
}
static class MapPersistentPropertyAccessor implements PersistentPropertyAccessor<Map<String, Object>> {
Map<String, Object> map = new LinkedHashMap<>();
@Override
public void setProperty(PersistentProperty<?> persistentProperty, Object o) {
map.put(persistentProperty.getName(), o);
}
@Override
public Object getProperty(PersistentProperty<?> persistentProperty) {
return map.get(persistentProperty.getName());
}
@Override
public Map<String, Object> getBean() {
return map;
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.core.MongoReader#read(java.lang.Class, com.mongodb.Document)
@ -423,7 +592,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -423,7 +592,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(contextToUse, documentAccessor,
evaluator);
readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator);
Predicate<MongoPersistentProperty> propertyFilter = isIdentifier(entity).or(isConstructorArgument(entity)).negate();
readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, propertyFilter);
return accessor.getBean();
}
@ -465,19 +635,28 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -465,19 +635,28 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private void readProperties(ConversionContext context, MongoPersistentEntity<?> entity,
PersistentPropertyAccessor<?> accessor, DocumentAccessor documentAccessor,
MongoDbPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator) {
MongoDbPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator,
Predicate<MongoPersistentProperty> propertyFilter) {
DbRefResolverCallback callback = null;
for (MongoPersistentProperty prop : entity) {
if (!propertyFilter.test(prop)) {
continue;
}
ConversionContext propertyContext = context.forProperty(prop.getName());
MongoDbPropertyValueProvider valueProviderToUse = valueProvider.withContext(propertyContext);
if (prop.isAssociation() && !entity.isConstructorArgument(prop)) {
if (callback == null) {
callback = getDbRefResolverCallback(context, documentAccessor, evaluator);
callback = getDbRefResolverCallback(propertyContext, documentAccessor, evaluator);
}
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context,
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback,
propertyContext,
evaluator);
continue;
}
@ -485,32 +664,27 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -485,32 +664,27 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (prop.isUnwrapped()) {
accessor.setProperty(prop,
readUnwrapped(context, documentAccessor, prop, mappingContext.getRequiredPersistentEntity(prop)));
continue;
}
// We skip the id property since it was already set
if (entity.isIdProperty(prop)) {
readUnwrapped(propertyContext, documentAccessor, prop, mappingContext.getRequiredPersistentEntity(prop)));
continue;
}
if (entity.isConstructorArgument(prop) || !documentAccessor.hasValue(prop)) {
if (!documentAccessor.hasValue(prop)) {
continue;
}
if (prop.isAssociation()) {
if (callback == null) {
callback = getDbRefResolverCallback(context, documentAccessor, evaluator);
callback = getDbRefResolverCallback(propertyContext, documentAccessor, evaluator);
}
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context,
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback,
propertyContext,
evaluator);
continue;
}
accessor.setProperty(prop, valueProvider.getPropertyValue(prop));
accessor.setProperty(prop, valueProviderToUse.getPropertyValue(prop));
}
}
@ -1716,6 +1890,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -1716,6 +1890,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return true;
}
static Predicate<MongoPersistentProperty> isIdentifier(PersistentEntity<?, ?> entity) {
return entity::isIdProperty;
}
static Predicate<MongoPersistentProperty> isConstructorArgument(PersistentEntity<?, ?> entity) {
return entity::isConstructorArgument;
}
/**
* {@link PropertyValueProvider} to evaluate a SpEL expression if present on the property or simply accesses the field
* of the configured source {@link Document}.
@ -1779,6 +1961,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -1779,6 +1961,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return (T) context.convert(value, property.getTypeInformation());
}
public MongoDbPropertyValueProvider withContext(ConversionContext context) {
if (context == this.context) {
return this;
}
return new MongoDbPropertyValueProvider(context, accessor, evaluator);
}
}
/**
@ -1997,13 +2188,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -1997,13 +2188,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/
protected static class ConversionContext {
private final org.springframework.data.convert.CustomConversions conversions;
private final ObjectPath path;
private final ContainerValueConverter<Bson> documentConverter;
private final ContainerValueConverter<Collection<?>> collectionConverter;
private final ContainerValueConverter<Bson> mapConverter;
private final ContainerValueConverter<DBRef> dbRefConverter;
private final ValueConverter<Object> elementConverter;
final org.springframework.data.convert.CustomConversions conversions;
final ObjectPath path;
final ContainerValueConverter<Bson> documentConverter;
final ContainerValueConverter<Collection<?>> collectionConverter;
final ContainerValueConverter<Bson> mapConverter;
final ContainerValueConverter<DBRef> dbRefConverter;
final ValueConverter<Object> elementConverter;
ConversionContext(org.springframework.data.convert.CustomConversions customConversions, ObjectPath path,
ContainerValueConverter<Bson> documentConverter, ContainerValueConverter<Collection<?>> collectionConverter,
@ -2099,6 +2290,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -2099,6 +2290,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return path;
}
public ConversionContext forProperty(String name) {
return this;
}
/**
* Converts a simple {@code source} value into {@link TypeInformation the target type}.
*
@ -2123,4 +2318,58 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -2123,4 +2318,58 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
}
private static class PropertyTranslatingPropertyAccessor<T> implements PersistentPropertyPathAccessor<T> {
private final PersistentPropertyAccessor<T> delegate;
private final PersistentPropertyTranslator propertyTranslator;
private PropertyTranslatingPropertyAccessor(PersistentPropertyAccessor<T> delegate,
PersistentPropertyTranslator propertyTranslator) {
this.delegate = delegate;
this.propertyTranslator = propertyTranslator;
}
static <T> PersistentPropertyAccessor<T> create(PersistentPropertyAccessor<T> delegate,
PersistentPropertyTranslator propertyTranslator) {
return new PropertyTranslatingPropertyAccessor<>(delegate, propertyTranslator);
}
@Override
public void setProperty(PersistentProperty property, @Nullable Object value) {
delegate.setProperty(translate(property), value);
}
@Override
public Object getProperty(PersistentProperty<?> property) {
return delegate.getProperty(translate(property));
}
@Override
public T getBean() {
return delegate.getBean();
}
@Override
public void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, Object value,
AccessOptions.SetOptions options) {
throw new UnsupportedOperationException();
}
@Override
public Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path,
AccessOptions.GetOptions context) {
throw new UnsupportedOperationException();
}
@Override
public void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, Object value) {
throw new UnsupportedOperationException();
}
private MongoPersistentProperty translate(PersistentProperty<?> property) {
return propertyTranslator.translate((MongoPersistentProperty) property);
}
}
}

34
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java

@ -19,13 +19,17 @@ import org.bson.BsonValue; @@ -19,13 +19,17 @@ import org.bson.BsonValue;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.springframework.core.convert.ConversionException;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.EntityConverter;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.convert.TypeMapper;
import org.springframework.data.mapping.context.EntityProjection;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -54,6 +58,35 @@ public interface MongoConverter @@ -54,6 +58,35 @@ public interface MongoConverter
*/
MongoTypeMapper getTypeMapper();
/**
* Returns the {@link ProjectionFactory} for this converter.
*
* @return will never be {@literal null}.
* @since 3.4
*/
ProjectionFactory getProjectionFactory();
/**
* Returns the {@link CustomConversions} for this converter.
*
* @return will never be {@literal null}.
* @since 3.4
*/
CustomConversions getCustomConversions();
/**
* Apply a projection to {@link Bson} and return the projection return type {@code R}.
* {@link EntityProjection#isProjection() Non-projecting} descriptors fall back to {@link #read(Class, Object) regular
* object materialization}.
*
* @param descriptor the projection descriptor, must not be {@literal null}.
* @param bson must not be {@literal null}.
* @param <R>
* @return a new instance of the projection return type {@code R}.
* @since 3.4
*/
<R> R project(EntityProjection<R, ?> descriptor, Bson bson);
/**
* Mapping function capable of converting values into a desired target type by eg. extracting the actual java type
* from a given {@link BsonValue}.
@ -154,4 +187,5 @@ public interface MongoConverter @@ -154,4 +187,5 @@ public interface MongoConverter
return convertToMongoType(id,(TypeInformation<?>) null);
}
}
}

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

@ -223,8 +223,8 @@ public class QueryMapper { @@ -223,8 +223,8 @@ public class QueryMapper {
if (fields.isEmpty()) {
return BsonUtils.EMPTY_DOCUMENT;
}
Document target = new Document();
BsonUtils.asMap(filterUnwrappedObjects(fields, entity)).forEach((k, v) -> {

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java

@ -201,7 +201,7 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope @@ -201,7 +201,7 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
* {@link org.springframework.data.mongodb.core.mapping.Field#value()} present.
* @since 1.7
*/
protected boolean hasExplicitFieldName() {
public boolean hasExplicitFieldName() {
return StringUtils.hasText(getAnnotatedFieldName());
}

7
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java

@ -41,6 +41,13 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist @@ -41,6 +41,13 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
*/
String getFieldName();
/**
* Returns whether the property uses an annotated field name through {@link Field}.
*
* @return
*/
boolean hasExplicitFieldName();
/**
* Returns the {@link Class Java FieldType} of the field a property is persisted to.
*

92
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/PersistentPropertyTranslator.java

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
/*
* Copyright 2021 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
*
* https://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.mapping;
import java.util.function.Predicate;
import org.springframework.data.util.Predicates;
import org.springframework.lang.Nullable;
/**
* Utility to translate a {@link MongoPersistentProperty} into a corresponding property from a different
* {@link MongoPersistentEntity} by looking it up by name.
* <p>
* Mainly used within the framework.
*
* @author Mark Paluch
* @since 3.4
*/
public class PersistentPropertyTranslator {
/**
* Translate a {@link MongoPersistentProperty} into a corresponding property from a different
* {@link MongoPersistentEntity}.
*
* @param property must not be {@literal null}.
* @return the translated property. Can be the original {@code property}.
*/
public MongoPersistentProperty translate(MongoPersistentProperty property) {
return property;
}
/**
* Create a new {@link PersistentPropertyTranslator}.
*
* @param targetEntity must not be {@literal null}.
* @return the property translator to use.
*/
public static PersistentPropertyTranslator create(@Nullable MongoPersistentEntity<?> targetEntity) {
return create(targetEntity, Predicates.isTrue());
}
/**
* Create a new {@link PersistentPropertyTranslator} accepting a {@link Predicate filter predicate} whether the
* translation should happen at all.
*
* @param targetEntity must not be {@literal null}.
* @param translationFilter must not be {@literal null}.
* @return the property translator to use.
*/
public static PersistentPropertyTranslator create(@Nullable MongoPersistentEntity<?> targetEntity,
Predicate<MongoPersistentProperty> translationFilter) {
return targetEntity != null ? new EntityPropertyTranslator(targetEntity, translationFilter)
: new PersistentPropertyTranslator();
}
private static class EntityPropertyTranslator extends PersistentPropertyTranslator {
private final MongoPersistentEntity<?> targetEntity;
private final Predicate<MongoPersistentProperty> translationFilter;
EntityPropertyTranslator(MongoPersistentEntity<?> targetEntity,
Predicate<MongoPersistentProperty> translationFilter) {
this.targetEntity = targetEntity;
this.translationFilter = translationFilter;
}
@Override
public MongoPersistentProperty translate(MongoPersistentProperty property) {
if (!translationFilter.test(property)) {
return property;
}
MongoPersistentProperty targetProperty = targetEntity.getPersistentProperty(property.getName());
return targetProperty != null ? targetProperty : property;
}
}
}

6
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java

@ -56,6 +56,12 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty { @@ -56,6 +56,12 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty {
return context.getProperty().findAnnotation(Unwrapped.class).prefix() + delegate.getFieldName();
}
@Override
public boolean hasExplicitFieldName() {
return delegate.hasExplicitFieldName()
|| !ObjectUtils.isEmpty(context.getProperty().findAnnotation(Unwrapped.class).prefix());
}
@Override
public Class<?> getFieldType() {
return delegate.getFieldType();

6
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java

@ -21,6 +21,7 @@ import java.io.Serializable; @@ -21,6 +21,7 @@ import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Optional;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.MongoOperations;
@ -76,6 +77,11 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { @@ -76,6 +77,11 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport {
this.mappingContext = mongoOperations.getConverter().getMappingContext();
}
@Override
protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) {
return this.operations.getConverter().getProjectionFactory();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata)

6
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java

@ -21,6 +21,7 @@ import java.io.Serializable; @@ -21,6 +21,7 @@ import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Optional;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
@ -78,6 +79,11 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup @@ -78,6 +79,11 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup
setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
}
@Override
protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) {
return this.operations.getConverter().getProjectionFactory();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata)

4
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java

@ -24,6 +24,8 @@ import org.springframework.core.convert.ConversionService; @@ -24,6 +24,8 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
/**
@ -37,7 +39,7 @@ public class EntityOperationUnitTests { @@ -37,7 +39,7 @@ public class EntityOperationUnitTests {
@BeforeEach
public void setUp() {
ops = new EntityOperations(mappingContext);
ops = new EntityOperations(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext));
}
@Test // DATAMONGO-2293

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

@ -22,7 +22,8 @@ import java.time.Instant; @@ -22,7 +22,8 @@ import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.mapping.TimeSeries;
import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
@ -33,7 +34,8 @@ import org.springframework.data.mongodb.test.util.MongoTestMappingContext; @@ -33,7 +34,8 @@ import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
*/
class EntityOperationsUnitTests {
EntityOperations operations = new EntityOperations(MongoTestMappingContext.newTestContext());
EntityOperations operations = new EntityOperations(
new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, MongoTestMappingContext.newTestContext()));
@Test // GH-3731
void shouldReportInvalidTimeField() {

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

@ -21,7 +21,9 @@ import static org.springframework.data.mongodb.core.query.Query.*; @@ -21,7 +21,9 @@ import static org.springframework.data.mongodb.core.query.Query.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Date;
import java.util.stream.Stream;
@ -32,6 +34,7 @@ import org.bson.Document; @@ -32,6 +34,7 @@ import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
@ -569,6 +572,8 @@ class ExecutableFindOperationSupportTests { @@ -569,6 +572,8 @@ class ExecutableFindOperationSupportTests {
String getName();
}
@Getter
@Setter // TODO: Without getters/setters, not identified as projection/properties
static class PersonDtoProjection {
@Field("firstname") String name;

2
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java

@ -27,6 +27,7 @@ import org.bson.BsonString; @@ -27,6 +27,7 @@ import org.bson.BsonString;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Query;
@ -278,7 +279,6 @@ class ExecutableUpdateOperationSupportTests { @@ -278,7 +279,6 @@ class ExecutableUpdateOperationSupportTests {
@Data
static class Jedi {
@Field("firstname") String name;
}
}

19
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoOperationsUnitTests.java

@ -27,9 +27,12 @@ import org.junit.jupiter.api.Test; @@ -27,9 +27,12 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.dao.DataAccessException;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.geo.Point;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.context.EntityProjection;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.convert.AbstractMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
@ -37,6 +40,7 @@ import org.springframework.data.mongodb.core.convert.MongoTypeMapper; @@ -37,6 +40,7 @@ import org.springframework.data.mongodb.core.convert.MongoTypeMapper;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.util.TypeInformation;
import com.mongodb.DBRef;
@ -92,6 +96,21 @@ public abstract class MongoOperationsUnitTests { @@ -92,6 +96,21 @@ public abstract class MongoOperationsUnitTests {
public MongoTypeMapper getTypeMapper() {
return null;
}
@Override
public ProjectionFactory getProjectionFactory() {
return null;
}
@Override
public CustomConversions getCustomConversions() {
return null;
}
@Override
public <R> R project(EntityProjection<R, ?> descriptor, Bson bson) {
return null;
}
};
}

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

@ -102,6 +102,7 @@ import org.springframework.data.mongodb.core.query.Query; @@ -102,6 +102,7 @@ import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.lang.Nullable;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.CollectionUtils;
@ -410,6 +411,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @@ -410,6 +411,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
when(cursor.next()).thenReturn(new org.bson.Document("_id", Integer.valueOf(0)));
MappingMongoConverter converter = mock(MappingMongoConverter.class);
when(converter.getMappingContext()).thenReturn((MappingContext) mappingContext);
when(converter.getProjectionFactory()).thenReturn(new SpelAwareProxyProjectionFactory());
template = new MongoTemplate(factory, converter);
assertThatExceptionOfType(MappingException.class).isThrownBy(() -> template.findAll(Person.class))

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

@ -90,6 +90,7 @@ import org.springframework.data.mongodb.core.query.NearQuery; @@ -90,6 +90,7 @@ 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.timeseries.Granularity;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.lang.Nullable;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.CollectionUtils;
@ -1151,6 +1152,7 @@ public class ReactiveMongoTemplateUnitTests { @@ -1151,6 +1152,7 @@ public class ReactiveMongoTemplateUnitTests {
MappingMongoConverter converter = mock(MappingMongoConverter.class);
when(converter.getMappingContext()).thenReturn((MappingContext) mappingContext);
when(converter.getProjectionFactory()).thenReturn(new SpelAwareProxyProjectionFactory());
template = new ReactiveMongoTemplate(factory, converter);
when(collection.find(Document.class)).thenReturn(findPublisher);
@ -1480,7 +1482,6 @@ public class ReactiveMongoTemplateUnitTests { @@ -1480,7 +1482,6 @@ public class ReactiveMongoTemplateUnitTests {
AutogenerateableId foo;
}
static class PersonExtended extends Person {
String lastname;

4
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java

@ -48,8 +48,8 @@ class UpdateOperationsUnitTests { @@ -48,8 +48,8 @@ class UpdateOperationsUnitTests {
MongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
QueryMapper queryMapper = new QueryMapper(mongoConverter);
UpdateMapper updateMapper = new UpdateMapper(mongoConverter);
EntityOperations entityOperations = new EntityOperations(mappingContext);
PropertyOperations propertyOperations = new PropertyOperations(mappingContext);
EntityOperations entityOperations = new EntityOperations(mongoConverter);
PropertyOperations propertyOperations = new PropertyOperations(mongoConverter.getMappingContext());
ExtendedQueryOperations queryOperations = new ExtendedQueryOperations(queryMapper, updateMapper, entityOperations, propertyOperations,
MongoClientSettings::getDefaultCodecRegistry);

19
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverterUnitTests.java

@ -19,13 +19,17 @@ import static org.mockito.Mockito.*; @@ -19,13 +19,17 @@ import static org.mockito.Mockito.*;
import org.bson.conversions.Bson;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mapping.context.EntityProjection;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.convert.MongoConverters.ObjectIdToStringConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToObjectIdConverter;
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.util.TypeInformation;
import com.mongodb.DBRef;
@ -59,6 +63,21 @@ public class AbstractMongoConverterUnitTests { @@ -59,6 +63,21 @@ public class AbstractMongoConverterUnitTests {
throw new UnsupportedOperationException();
}
@Override
public ProjectionFactory getProjectionFactory() {
return null;
}
@Override
public CustomConversions getCustomConversions() {
return null;
}
@Override
public <R> R project(EntityProjection<R, ?> descriptor, Bson bson) {
return null;
}
@Override
public MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> getMappingContext() {
throw new UnsupportedOperationException();

138
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java

@ -21,6 +21,7 @@ import static org.mockito.Mockito.*; @@ -21,6 +21,7 @@ import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.DocumentTestUtils.*;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.math.BigDecimal;
@ -66,6 +67,8 @@ import org.springframework.data.geo.Polygon; @@ -66,6 +67,8 @@ import org.springframework.data.geo.Polygon;
import org.springframework.data.geo.Shape;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.EntityProjection;
import org.springframework.data.mapping.context.EntityProjectionIntrospector;
import org.springframework.data.mapping.model.MappingInstantiationException;
import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.NestedType;
@ -138,7 +141,7 @@ class MappingMongoConverterUnitTests { @@ -138,7 +141,7 @@ class MappingMongoConverterUnitTests {
converter.write(address, document);
assertThat(document.get("city").toString()).isEqualTo("New York");
assertThat(document.get("street").toString()).isEqualTo("Broadway");
assertThat(document.get("s").toString()).isEqualTo("Broadway");
}
@Test
@ -2191,7 +2194,8 @@ class MappingMongoConverterUnitTests { @@ -2191,7 +2194,8 @@ class MappingMongoConverterUnitTests {
@Test // GH-3546
void readFlattensNestedDocumentToStringIfNecessary() {
org.bson.Document source = new org.bson.Document("street", new org.bson.Document("json", "string").append("_id", UUID.randomUUID()));
org.bson.Document source = new org.bson.Document("s",
new org.bson.Document("json", "string").append("_id", UUID.randomUUID()));
Address target = converter.read(Address.class, source);
assertThat(target.street).isNotNull();
@ -2355,7 +2359,7 @@ class MappingMongoConverterUnitTests { @@ -2355,7 +2359,7 @@ class MappingMongoConverterUnitTests {
void readUnwrappedTypeWithComplexValue() {
org.bson.Document source = new org.bson.Document("_id", "id-1").append("address",
new org.bson.Document("street", "1007 Mountain Drive").append("city", "Gotham"));
new org.bson.Document("s", "1007 Mountain Drive").append("city", "Gotham"));
WithNullableUnwrapped target = converter.read(WithNullableUnwrapped.class, source);
@ -2381,9 +2385,9 @@ class MappingMongoConverterUnitTests { @@ -2381,9 +2385,9 @@ class MappingMongoConverterUnitTests {
converter.write(source, target);
assertThat(target) //
.containsEntry("address", new org.bson.Document("street", "1007 Mountain Drive").append("city", "Gotham")) //
.containsEntry("address", new org.bson.Document("s", "1007 Mountain Drive").append("city", "Gotham")) //
.doesNotContainKey("street") //
.doesNotContainKey("address.street") //
.doesNotContainKey("address.s") //
.doesNotContainKey("city") //
.doesNotContainKey("address.city");
}
@ -2636,6 +2640,80 @@ class MappingMongoConverterUnitTests { @@ -2636,6 +2640,80 @@ class MappingMongoConverterUnitTests {
assertThat(accessor.getDocument()).isEqualTo(new org.bson.Document("pName", new org.bson.Document("_id", id.toString())));
}
@Test // GH-2860
void projectShouldReadSimpleInterfaceProjection() {
org.bson.Document source = new org.bson.Document("birthDate", new LocalDate(1999, 12, 1).toDate()).append("foo",
"Walter");
EntityProjectionIntrospector discoverer = EntityProjectionIntrospector.create(converter.getProjectionFactory(),
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
.and((target, underlyingType) -> !converter.conversions.isSimpleType(target)),
mappingContext);
EntityProjection<PersonProjection, Person> projection = discoverer
.introspect(PersonProjection.class, Person.class);
PersonProjection person = converter.project(projection, source);
assertThat(person.getBirthDate()).isEqualTo(new LocalDate(1999, 12, 1));
assertThat(person.getFirstname()).isEqualTo("Walter");
}
@Test // GH-2860
void projectShouldReadSimpleDtoProjection() {
org.bson.Document source = new org.bson.Document("birthDate", new LocalDate(1999, 12, 1).toDate()).append("foo",
"Walter");
EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(),
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
.and((target, underlyingType) -> !converter.conversions.isSimpleType(target)),
mappingContext);
EntityProjection<PersonDto, Person> projection = introspector
.introspect(PersonDto.class, Person.class);
PersonDto person = converter.project(projection, source);
assertThat(person.getBirthDate()).isEqualTo(new LocalDate(1999, 12, 1));
assertThat(person.getFirstname()).isEqualTo("Walter");
}
@Test // GH-2860
void projectShouldReadNestedProjection() {
org.bson.Document source = new org.bson.Document("addresses",
Collections.singletonList(new org.bson.Document("s", "hwy")));
EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(),
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
.and((target, underlyingType) -> !converter.conversions.isSimpleType(target)),
mappingContext);
EntityProjection<WithNestedProjection, Person> projection = introspector
.introspect(WithNestedProjection.class, Person.class);
WithNestedProjection person = converter.project(projection, source);
assertThat(person.getAddresses()).extracting(AddressProjection::getStreet).hasSize(1).containsOnly("hwy");
}
@Test // GH-2860
void projectShouldReadProjectionWithNestedEntity() {
org.bson.Document source = new org.bson.Document("addresses",
Collections.singletonList(new org.bson.Document("s", "hwy")));
EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(),
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
.and((target, underlyingType) -> !converter.conversions.isSimpleType(target)),
mappingContext);
EntityProjection<ProjectionWithNestedEntity, Person> projection = introspector
.introspect(ProjectionWithNestedEntity.class, Person.class);
ProjectionWithNestedEntity person = converter.project(projection, source);
assertThat(person.getAddresses()).extracting(Address::getStreet).hasSize(1).containsOnly("hwy");
}
static class GenericType<T> {
T content;
}
@ -2666,7 +2744,9 @@ class MappingMongoConverterUnitTests { @@ -2666,7 +2744,9 @@ class MappingMongoConverterUnitTests {
}
@EqualsAndHashCode
@Getter
static class Address implements InterfaceType {
@Field("s")
String street;
String city;
}
@ -2696,6 +2776,54 @@ class MappingMongoConverterUnitTests { @@ -2696,6 +2776,54 @@ class MappingMongoConverterUnitTests {
}
}
interface PersonProjection {
LocalDate getBirthDate();
String getFirstname();
}
interface WithNestedProjection {
Set<AddressProjection> getAddresses();
}
interface ProjectionWithNestedEntity {
Set<Address> getAddresses();
}
interface AddressProjection {
String getStreet();
}
static class PersonDto {
LocalDate birthDate;
@Field("foo") String firstname;
String lastname;
public PersonDto(LocalDate birthDate, String firstname, String lastname) {
this.birthDate = birthDate;
this.firstname = firstname;
this.lastname = lastname;
}
public LocalDate getBirthDate() {
return birthDate;
}
public String getFirstname() {
return firstname;
}
public String getLastname() {
return lastname;
}
}
static class ClassWithSortedMap {
SortedMap<String, String> map;
}

5
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java

@ -25,6 +25,8 @@ import org.junit.jupiter.api.Test; @@ -25,6 +25,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.MongoTemplate;
@ -32,6 +34,7 @@ import org.springframework.data.mongodb.core.convert.MongoConverter; @@ -32,6 +34,7 @@ import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
/**
@ -40,6 +43,7 @@ import org.springframework.data.repository.Repository; @@ -40,6 +43,7 @@ import org.springframework.data.repository.Repository;
* @author Oliver Gierke
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class MongoRepositoryFactoryUnitTests {
@Mock MongoTemplate template;
@ -55,6 +59,7 @@ public class MongoRepositoryFactoryUnitTests { @@ -55,6 +59,7 @@ public class MongoRepositoryFactoryUnitTests {
public void setUp() {
when(template.getConverter()).thenReturn(converter);
when(converter.getMappingContext()).thenReturn(mappingContext);
when(converter.getProjectionFactory()).thenReturn(new SpelAwareProxyProjectionFactory());
}
@Test

Loading…
Cancel
Save