diff --git a/pom.xml b/pom.xml index b6410e6887..619a86e88c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 3.4.0-SNAPSHOT + 3.4.0-2860-SNAPSHOT pom Spring Data MongoDB @@ -26,7 +26,7 @@ multi spring-data-mongodb - 2.7.0-SNAPSHOT + 2.7.0-GH-2420-SNAPSHOT 4.4.0 ${mongo} 1.19 diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index e2704a6753..5acd398513 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 3.4.0-SNAPSHOT + 3.4.0-2860-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index b75f8bf624..4380676a50 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-mongodb-parent - 3.4.0-SNAPSHOT + 3.4.0-2860-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index ca96626cc9..b0bdd93cf4 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 3.4.0-SNAPSHOT + 3.4.0-2860-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java index 3bba17aaef..b5af067589 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -23,13 +23,17 @@ 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.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 { private final MappingContext, MongoPersistentProperty> context; - EntityOperations(MappingContext, MongoPersistentProperty> context) { + private final EntityProjectionIntrospector introspector; + + EntityOperations(MongoConverter converter) { + this(converter.getMappingContext(), converter.getCustomConversions(), converter.getProjectionFactory()); + } + + EntityOperations(MappingContext, 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 @@ public TypedOperations forType(@Nullable Class entityClass) { return UntypedOperations.instance(); } + public EntityProjection introspectProjection(Class resultType, + Class entityType) { + return introspector.introspect(resultType, entityType); + } + /** * A representation of information about an entity. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 895097dd47..15b6b5b790 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -49,6 +49,7 @@ 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.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, 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 MongoTemplate(MongoDatabaseFactory mongoDbFactory, @Nullable MongoConvert 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 @@ private MongoTemplate(MongoDatabaseFactory dbFactory, MongoTemplate that) { 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 void setApplicationContext(ApplicationContext applicationContext) throws } resourceLoader = applicationContext; - - projectionFactory.setBeanFactory(applicationContext); - projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); } /** @@ -416,15 +410,17 @@ protected CloseableIterator doStream(Query query, Class entityType, St MongoPersistentEntity persistentEntity = mappingContext.getPersistentEntity(entityType); QueryContext queryContext = queryOperations.createQueryContext(query); + EntityProjection projection = operations.introspectProjection(returnType, + entityType); Document mappedQuery = queryContext.getMappedQuery(persistentEntity); - Document mappedFields = queryContext.getMappedFields(persistentEntity, returnType, projectionFactory); + Document mappedFields = queryContext.getMappedFields(persistentEntity, projection); FindIterable 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 GeoResults geoNear(NearQuery near, Class domainType, String col .withOptions(AggregationOptions.builder().collation(near.getCollation()).build()); AggregationResults results = aggregate($geoNear, collection, Document.class); + EntityProjection projection = operations.introspectProjection(returnType, + domainType); DocumentCallback> callback = new GeoNearResultDocumentCallback<>(distanceField, - new ProjectingReadCallback<>(mongoConverter, domainType, returnType, collection), near.getMetric()); + new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric()); List> result = new ArrayList<>(); @@ -1050,8 +1048,10 @@ public T findAndReplace(Query query, S replacement, FindAndReplaceOptions MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityType); QueryContext queryContext = queryOperations.createQueryContext(query); + EntityProjection 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 T findAndReplace(Query query, S replacement, FindAndReplaceOptions 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 @@ protected T doFindOne(String collectionName, Document query, Document fields 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 @@ protected List doFind(String collectionName, Document query, Document 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 @@ List doFind(String collectionName, Document query, Document fields, Cl Class targetClass, CursorPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); + EntityProjection 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 @@ List doFind(String collectionName, Document query, Document fields, Cl } 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.
@@ -2745,6 +2751,35 @@ protected T doFindAndReplace(String collectionName, Document mappedQuery, Do Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { + EntityProjection 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 doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, + Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class entityType, + Document replacement, FindAndReplaceOptions options, + EntityProjection 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 @@ protected T doFindAndReplace(String collectionName, Document mappedQuery, Do 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 T doWith(Document document) { */ private class ProjectingReadCallback implements DocumentCallback { - private final EntityReader reader; - private final Class entityType; - private final Class targetType; + private final MongoConverter reader; + private final EntityProjection projection; private final String collectionName; - ProjectingReadCallback(EntityReader reader, Class entityType, Class targetType, + ProjectingReadCallback(MongoConverter reader, EntityProjection projection, String collectionName) { this.reader = reader; - this.entityType = entityType; - this.targetType = targetType; + this.projection = projection; this.collectionName = collectionName; } @@ -3230,21 +3263,16 @@ public T doWith(Document document) { 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); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java index 5eb9f110b6..eddad2ba41 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java @@ -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 { /** * 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); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java index e9431aa3d2..5bf6226bdc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java @@ -28,8 +28,10 @@ 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.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 @@ Document getMappedQuery(@Nullable MongoPersistentEntity entity) { return queryMapper.getMappedObject(getQueryObject(), entity); } - Document getMappedFields(@Nullable MongoPersistentEntity entity, Class targetType, - ProjectionFactory projectionFactory) { + Document getMappedFields(@Nullable MongoPersistentEntity entity, + EntityProjection projection) { - Document fields = new Document(); + Document fields = evaluateFields(entity); - for (Entry entry : query.getFieldsObject().entrySet()) { + if (entity == null) { + return fields; + } - if (entry.getValue() instanceof MongoExpression) { + Document mappedFields; + if (!fields.isEmpty()) { + mappedFields = queryMapper.getMappedFields(fields, entity); + } else { + mappedFields = propertyOperations.computeMappedFieldsForProjection(projection, fields); + } - AggregationOperationContext ctx = entity == null ? Aggregation.DEFAULT_CONTEXT - : new RelaxedTypeBasedAggregationOperationContext(entity.getType(), mappingContext, queryMapper); + if (entity.hasTextScoreProperty() && mappedFields.containsKey(entity.getTextScoreProperty().getFieldName()) + && !query.getQueryObject().containsKey("$text")) { + mappedFields.remove(entity.getTextScoreProperty().getFieldName()); + } - fields.put(entry.getKey(), AggregationExpression.from((MongoExpression) entry.getValue()).toDocument(ctx)); - } else { - fields.put(entry.getKey(), entry.getValue()); - } + if (mappedFields.isEmpty()) { + return BsonUtils.EMPTY_DOCUMENT; } - Document mappedFields = fields; + return mappedFields; + } + + private Document evaluateFields(@Nullable MongoPersistentEntity entity) { - if (entity == null) { - return mappedFields; + Document fields = query.getFieldsObject(); + + if (fields.isEmpty()) { + return BsonUtils.EMPTY_DOCUMENT; } - Document projectedFields = propertyOperations.computeFieldsForProjection(projectionFactory, fields, - entity.getType(), targetType); + Document evaluated = new Document(); - if (ObjectUtils.nullSafeEquals(fields, projectedFields)) { - mappedFields = queryMapper.getMappedFields(projectedFields, entity); - } else { - mappedFields = queryMapper.getMappedFields(projectedFields, - mappingContext.getRequiredPersistentEntity(targetType)); - } + for (Entry entry : fields.entrySet()) { - if (entity.hasTextScoreProperty() && !query.getQueryObject().containsKey("$text")) { - mappedFields.remove(entity.getTextScoreProperty().getFieldName()); + 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 @@ private DistinctQueryContext(@Nullable Object query, String fieldName) { } @Override - Document getMappedFields(@Nullable MongoPersistentEntity entity, Class targetType, - ProjectionFactory projectionFactory) { + Document getMappedFields(@Nullable MongoPersistentEntity entity, + EntityProjection projection) { return getMappedFields(entity); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index e408e62cdc..3cadc2c1b2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -63,6 +63,7 @@ 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.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 private final QueryMapper queryMapper; private final UpdateMapper updateMapper; private final JsonSchemaMapper schemaMapper; - private final SpelAwareProxyProjectionFactory projectionFactory; private final ApplicationListener> indexCreatorListener; private final EntityOperations operations; private final PropertyOperations propertyOperations; @@ -242,13 +241,12 @@ public ReactiveMongoTemplate(ReactiveMongoDatabaseFactory mongoDatabaseFactory, 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 @@ private ReactiveMongoTemplate(ReactiveMongoDatabaseFactory dbFactory, ReactiveMo 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 void setApplicationContext(ApplicationContext applicationContext) throws if (mappingContext instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher); } - - projectionFactory.setBeanFactory(applicationContext); - projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); } /** @@ -1058,9 +1052,11 @@ protected Flux> geoNear(NearQuery near, Class entityClass, S String collection = StringUtils.hasText(collectionName) ? collectionName : getCollectionName(entityClass); String distanceField = operations.nearQueryDistanceFieldName(entityClass); + EntityProjection projection = operations.introspectProjection(returnType, + entityClass); GeoNearResultDocumentCallback 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 Mono findAndReplace(Query query, S replacement, FindAndReplaceO MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityType); QueryContext queryContext = queryOperations.createQueryContext(query); + EntityProjection 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 Mono findAndReplace(Query query, S replacement, FindAndReplaceO }).flatMap(it -> { Mono 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 @@ protected Mono doFindOne(String collectionName, Document query, @Nullable 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 @@ protected Flux doFind(String collectionName, Document query, Document 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 @@ Flux doFind(String collectionName, Document query, Document fields, Cl Class targetClass, FindPublisherPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); + EntityProjection 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 @@ Flux doFind(String collectionName, Document query, Document fields, Cl } 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 @@ protected Mono doFindAndReplace(String collectionName, Document mappedQue Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { + EntityProjection 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 Mono doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, + Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, + FindAndReplaceOptions options, + EntityProjection projection) { + return Mono.defer(() -> { if (LOGGER.isDebugEnabled()) { @@ -2622,7 +2636,7 @@ protected Mono doFindAndReplace(String collectionName, Document mappedQue 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 Mono doWith(Document document) { */ private class ProjectingReadCallback implements DocumentCallback { - private final EntityReader reader; - private final Class entityType; - private final Class targetType; + private final MongoConverter reader; + private final EntityProjection projection; private final String collectionName; - ProjectingReadCallback(EntityReader reader, Class entityType, Class targetType, + ProjectingReadCallback(MongoConverter reader, EntityProjection projection, String collectionName) { this.reader = reader; - this.entityType = entityType; - this.targetType = targetType; + this.projection = projection; this.collectionName = collectionName; } @SuppressWarnings("unchecked") public Mono doWith(Document document) { - Class typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) // - ? entityType // - : targetType; - - maybeEmitEvent(new AfterLoadEvent<>(document, typeToRead, collectionName)); + Class returnType = projection.getMappedType().getType(); + maybeEmitEvent(new AfterLoadEvent<>(document, returnType, 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); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java index 0b31f75341..3afe41d1f6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java @@ -91,7 +91,7 @@ public void putAll(Document source) { 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 @@ public void put(MongoPersistentProperty prop, @Nullable Object value) { */ @Nullable public Object get(MongoPersistentProperty property) { - return BsonUtils.resolveValue(document, property.getFieldName()); + return BsonUtils.resolveValue(document, getFieldName(property)); } /** @@ -150,7 +150,11 @@ public boolean hasValue(MongoPersistentProperty property) { 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(); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index c8d2c56bad..5c7e049cca 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -27,6 +27,7 @@ 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; @@ -45,13 +46,20 @@ 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; @@ -68,6 +76,7 @@ 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; @@ -75,7 +84,10 @@ 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; @@ -127,6 +139,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 MongoTypeMapper getTypeMapper() { return 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 void setApplicationContext(ApplicationContext applicationContext) throws 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)); @@ -276,6 +301,150 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) { this.entityCallbacks = entityCallbacks; } + @Override + public R project(EntityProjection 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 doReadProjection(ConversionContext context, Bson bson, + EntityProjection projection) { + + MongoPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(projection.getActualDomainType()); + TypeInformation mappedType = projection.getActualMappedType(); + MongoPersistentEntity mappedEntity = (MongoPersistentEntity) 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 persistenceConstructor = mappedEntity.getPersistenceConstructor(); + ParameterValueProvider provider = persistenceConstructor != null + && persistenceConstructor.hasParameters() + ? getParameterProvider(context, mappedEntity, documentAccessor, evaluator) + : NoOpParameterValueProvider.INSTANCE; + + EntityInstantiator instantiator = instantiators.getInstantiatorFor(mappedEntity); + R instance = instantiator.createInstance(mappedEntity, provider); + PersistentPropertyAccessor 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> collectionConverter, ContainerValueConverter mapConverter, + ContainerValueConverter dbRefConverter, ValueConverter 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 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 getBean() { + return map; + } + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.core.MongoReader#read(java.lang.Class, com.mongodb.Document) @@ -418,7 +587,8 @@ private S populateProperties(ConversionContext context, MongoPersistentEntit MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(contextToUse, documentAccessor, evaluator); - readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator); + Predicate propertyFilter = isIdentifier(entity).or(isConstructorArgument(entity)).negate(); + readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, propertyFilter); return accessor.getBean(); } @@ -460,19 +630,28 @@ private Object readIdValue(ConversionContext context, SpELExpressionEvaluator ev private void readProperties(ConversionContext context, MongoPersistentEntity entity, PersistentPropertyAccessor accessor, DocumentAccessor documentAccessor, - MongoDbPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator) { + MongoDbPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator, + Predicate 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; } @@ -480,32 +659,27 @@ private void readProperties(ConversionContext context, MongoPersistentEntity 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)); } } @@ -1710,6 +1884,14 @@ private static boolean isCollectionOfDbRefWhereBulkFetchIsPossible(Iterable s return true; } + static Predicate isIdentifier(PersistentEntity entity) { + return entity::isIdProperty; + } + + static Predicate 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}. @@ -1773,6 +1955,15 @@ public T getPropertyValue(MongoPersistentProperty property) { return (T) context.convert(value, property.getTypeInformation()); } + + public MongoDbPropertyValueProvider withContext(ConversionContext context) { + if (context == this.context) { + return this; + } + + return new MongoDbPropertyValueProvider(context, accessor, evaluator); + + } } /** @@ -1991,13 +2182,13 @@ public org.springframework.data.util.TypeInformation specialize(Cla */ protected static class ConversionContext { - private final org.springframework.data.convert.CustomConversions conversions; - private final ObjectPath path; - private final ContainerValueConverter documentConverter; - private final ContainerValueConverter> collectionConverter; - private final ContainerValueConverter mapConverter; - private final ContainerValueConverter dbRefConverter; - private final ValueConverter elementConverter; + final org.springframework.data.convert.CustomConversions conversions; + final ObjectPath path; + final ContainerValueConverter documentConverter; + final ContainerValueConverter> collectionConverter; + final ContainerValueConverter mapConverter; + final ContainerValueConverter dbRefConverter; + final ValueConverter elementConverter; ConversionContext(org.springframework.data.convert.CustomConversions customConversions, ObjectPath path, ContainerValueConverter documentConverter, ContainerValueConverter> collectionConverter, @@ -2093,6 +2284,10 @@ public ObjectPath getPath() { return path; } + public ConversionContext forProperty(String name) { + return this; + } + /** * Converts a simple {@code source} value into {@link TypeInformation the target type}. * @@ -2117,4 +2312,58 @@ interface ContainerValueConverter { } } + + private static class PropertyTranslatingPropertyAccessor implements PersistentPropertyPathAccessor { + + private final PersistentPropertyAccessor delegate; + private final PersistentPropertyTranslator propertyTranslator; + + private PropertyTranslatingPropertyAccessor(PersistentPropertyAccessor delegate, + PersistentPropertyTranslator propertyTranslator) { + this.delegate = delegate; + this.propertyTranslator = propertyTranslator; + } + + static PersistentPropertyAccessor create(PersistentPropertyAccessor 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> path, Object value, + AccessOptions.SetOptions options) { + throw new UnsupportedOperationException(); + } + + @Override + public Object getProperty(PersistentPropertyPath> path, + AccessOptions.GetOptions context) { + throw new UnsupportedOperationException(); + } + + @Override + public void setProperty(PersistentPropertyPath> path, Object value) { + throw new UnsupportedOperationException(); + } + + private MongoPersistentProperty translate(PersistentProperty property) { + return propertyTranslator.translate((MongoPersistentProperty) property); + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java index aff1b8d8e0..c0a3e31781 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java @@ -19,13 +19,17 @@ 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 */ 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 + * @return a new instance of the projection return type {@code R}. + * @since 3.4 + */ + R project(EntityProjection 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 @@ default Object convertId(@Nullable Object id, Class targetType) { return convertToMongoType(id,(TypeInformation) null); } } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 1ae0a15358..d97070aab3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -223,8 +223,8 @@ private Document mapFieldsToPropertyNames(Document fields, @Nullable MongoPersis if (fields.isEmpty()) { return BsonUtils.EMPTY_DOCUMENT; - } + Document target = new Document(); BsonUtils.asMap(filterUnwrappedObjects(fields, entity)).forEach((k, v) -> { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java index 66c88cf9bf..80d10c4145 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java @@ -201,7 +201,7 @@ public Class getFieldType() { * {@link org.springframework.data.mongodb.core.mapping.Field#value()} present. * @since 1.7 */ - protected boolean hasExplicitFieldName() { + public boolean hasExplicitFieldName() { return StringUtils.hasText(getAnnotatedFieldName()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java index 8dc89e03f9..cb31bdb74b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java @@ -41,6 +41,13 @@ public interface MongoPersistentProperty extends PersistentProperty + * 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 translationFilter) { + return targetEntity != null ? new EntityPropertyTranslator(targetEntity, translationFilter) + : new PersistentPropertyTranslator(); + } + + private static class EntityPropertyTranslator extends PersistentPropertyTranslator { + + private final MongoPersistentEntity targetEntity; + private final Predicate translationFilter; + + EntityPropertyTranslator(MongoPersistentEntity targetEntity, + Predicate 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; + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java index 24e4ae057f..7ad3e00744 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java @@ -56,6 +56,12 @@ public String getFieldName() { 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(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index 5a023b2b09..836f04b6ea 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -21,6 +21,7 @@ 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 MongoRepositoryFactory(MongoOperations mongoOperations) { 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) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index 9969859076..6476550be7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -21,6 +21,7 @@ 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 ReactiveMongoRepositoryFactory(ReactiveMongoOperations mongoOperations) { 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) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java index d24a8e8027..4b26ba69af 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java @@ -24,6 +24,8 @@ 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 { @BeforeEach public void setUp() { - ops = new EntityOperations(mappingContext); + ops = new EntityOperations(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)); } @Test // DATAMONGO-2293 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java index 160a598bc7..9c6b750c3a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java @@ -22,7 +22,8 @@ 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 @@ */ class EntityOperationsUnitTests { - EntityOperations operations = new EntityOperations(MongoTestMappingContext.newTestContext()); + EntityOperations operations = new EntityOperations( + new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, MongoTestMappingContext.newTestContext())); @Test // GH-3731 void shouldReportInvalidTimeField() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java index 8ebf72e130..5007709023 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java @@ -21,7 +21,9 @@ 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.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; @@ -41,6 +44,8 @@ import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; import org.springframework.data.mongodb.core.index.GeospatialIndex; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.NearQuery; @@ -546,6 +551,70 @@ void distinctAppliesFilterQuery() { ).containsExactlyInAnyOrder("luke"); } + @Test // GH-2860 + void projectionOnDbRef() { + + WithRefs source = new WithRefs(); + source.id = "id-1"; + source.noRef = "value"; + source.planetDbRef = alderan; + + template.save(source); + + WithDbRefProjection target = template.query(WithRefs.class).as(WithDbRefProjection.class) + .matching(where("id").is(source.id)).oneValue(); + + assertThat(target.getPlanetDbRef()).isEqualTo(alderan); + } + + @Test // GH-2860 + void propertyProjectionOnDbRef() { + + WithRefs source = new WithRefs(); + source.id = "id-1"; + source.noRef = "value"; + source.planetDbRef = alderan; + + template.save(source); + + WithDbRefPropertyProjection target = template.query(WithRefs.class).as(WithDbRefPropertyProjection.class) + .matching(where("id").is(source.id)).oneValue(); + + assertThat(target.getPlanetDbRef().getName()).isEqualTo(alderan.getName()); + } + + @Test // GH-2860 + void projectionOnDocRef() { + + WithRefs source = new WithRefs(); + source.id = "id-1"; + source.noRef = "value"; + source.planetDocRef = alderan; + + template.save(source); + + WithDocumentRefProjection target = template.query(WithRefs.class).as(WithDocumentRefProjection.class) + .matching(where("id").is(source.id)).oneValue(); + + assertThat(target.getPlanetDocRef()).isEqualTo(alderan); + } + + @Test // GH-2860 + void propertyProjectionOnDocRef() { + + WithRefs source = new WithRefs(); + source.id = "id-1"; + source.noRef = "value"; + source.planetDocRef = alderan; + + template.save(source); + + WithDocRefPropertyProjection target = template.query(WithRefs.class).as(WithDocRefPropertyProjection.class) + .matching(where("id").is(source.id)).oneValue(); + + assertThat(target.getPlanetDocRef().getName()).isEqualTo(alderan.getName()); + } + interface Contact {} @Data @@ -569,6 +638,8 @@ public interface PersonSpELProjection { String getName(); } + @Getter + @Setter // TODO: Without getters/setters, not identified as projection/properties static class PersonDtoProjection { @Field("firstname") String name; @@ -613,6 +684,34 @@ interface PlanetSpELProjection { String getId(); } + @Data + static class WithRefs { + + @Id String id; + + String noRef; + + @DBRef Planet planetDbRef; + + @DocumentReference Planet planetDocRef; + } + + interface WithDbRefProjection { + Planet getPlanetDbRef(); + } + + interface WithDocumentRefProjection { + Planet getPlanetDocRef(); + } + + interface WithDbRefPropertyProjection { + PlanetProjection getPlanetDbRef(); + } + + interface WithDocRefPropertyProjection { + PlanetProjection getPlanetDocRef(); + } + private void initPersons() { han = new Person(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java index d103b73614..f772fd462c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java @@ -27,6 +27,7 @@ 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 @@ static class Human { @Data static class Jedi { - @Field("firstname") String name; } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoOperationsUnitTests.java index 3bbff3fcde..24d000773e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoOperationsUnitTests.java @@ -27,9 +27,12 @@ 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.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 DBRef toDBRef(Object object, MongoPersistentProperty referingProperty) { public MongoTypeMapper getTypeMapper() { return null; } + + @Override + public ProjectionFactory getProjectionFactory() { + return null; + } + + @Override + public CustomConversions getCustomConversions() { + return null; + } + + @Override + public R project(EntityProjection descriptor, Bson bson) { + return null; + } }; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 3d6278d4b9..34a43c9103 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -102,6 +102,7 @@ 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 @@ void shouldThrowExceptionIfEntityReaderReturnsNull() { 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)) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index 10e4f1cfcc..9ee4244219 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -90,6 +90,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.projection.SpelAwareProxyProjectionFactory; import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; @@ -1151,6 +1152,7 @@ void shouldThrowExceptionIfEntityReaderReturnsNull() { 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 @@ class Wrapper { AutogenerateableId foo; } - static class PersonExtended extends Person { String lastname; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java index 4d1d5a4264..989ea8b603 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java @@ -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); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverterUnitTests.java index b1d4a61204..fcc10e49a1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverterUnitTests.java @@ -19,13 +19,17 @@ 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 MongoTypeMapper getTypeMapper() { throw new UnsupportedOperationException(); } + @Override + public ProjectionFactory getProjectionFactory() { + return null; + } + + @Override + public CustomConversions getCustomConversions() { + return null; + } + + @Override + public R project(EntityProjection descriptor, Bson bson) { + return null; + } + @Override public MappingContext, MongoPersistentProperty> getMappingContext() { throw new UnsupportedOperationException(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index d4c735fd23..a5200c5ec0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -21,6 +21,7 @@ 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.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 @@ void convertsAddressCorrectly() { 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 @@ void readAndConvertDBRefNestedByMapCorrectly() { @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 @@ void readDeepNestedUnwrappedType() { 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 @@ void writeUnwrappedTypeWithComplexValue() { 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 @@ void readsMapThatDoesNotComeAsDocument() { 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 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 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 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 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 content; } @@ -2666,7 +2744,9 @@ interface InterfaceType { } @EqualsAndHashCode + @Getter static class Address implements InterfaceType { + @Field("s") String street; String city; } @@ -2696,6 +2776,54 @@ public Person(Set
addresses) { } } + interface PersonProjection { + + LocalDate getBirthDate(); + + String getFirstname(); + } + + interface WithNestedProjection { + + Set getAddresses(); + } + + interface ProjectionWithNestedEntity { + + Set
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 map; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java index 625f0c1995..263da4e743 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java @@ -25,6 +25,8 @@ 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.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 @@ * @author Oliver Gierke */ @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class MongoRepositoryFactoryUnitTests { @Mock MongoTemplate template; @@ -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