From fdff74f7b5b6697b7d07ef2280b48d060c876169 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 10 Dec 2021 09:11:47 +0100 Subject: [PATCH] Polishing. Add tests for projections on dbref properties update java- and reference documentation. Original Pull Request: #3894 --- .../data/mongodb/core/EntityOperations.java | 13 ++- .../data/mongodb/core/MongoTemplate.java | 13 ++- .../data/mongodb/core/PropertyOperations.java | 2 +- .../mongodb/core/ReactiveMongoTemplate.java | 3 +- .../ExecutableFindOperationSupportTests.java | 96 +++++++++++++++++++ src/main/asciidoc/reference/mongodb.adoc | 2 + 6 files changed, 117 insertions(+), 12 deletions(-) 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 b5af06758..f9df45ca1 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 @@ -245,8 +245,17 @@ class EntityOperations { return UntypedOperations.instance(); } - public EntityProjection introspectProjection(Class resultType, - Class entityType) { + /** + * Introspect the given {@link Class result type} in the context of the {@link Class entity type} whether the returned + * type is a projection and what property paths are participating in the projection. + * + * @param resultType the type to project on. Must not be {@literal null}. + * @param entityType the source domain type. Must not be {@literal null}. + * @return the introspection result. + * @since 3.4 + * @see EntityProjectionIntrospector#introspect(Class, Class) + */ + public EntityProjection introspectProjection(Class resultType, Class entityType) { return introspector.introspect(resultType, entityType); } 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 15b6b5b79..7afb316ed 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 @@ -2777,8 +2777,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @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) { + Document replacement, FindAndReplaceOptions options, EntityProjection projection) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format( @@ -3240,14 +3239,14 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ private class ProjectingReadCallback implements DocumentCallback { - private final MongoConverter reader; + private final MongoConverter mongoConverter; private final EntityProjection projection; private final String collectionName; - ProjectingReadCallback(MongoConverter reader, EntityProjection projection, + ProjectingReadCallback(MongoConverter mongoConverter, EntityProjection projection, String collectionName) { - this.reader = reader; + this.mongoConverter = mongoConverter; this.projection = projection; this.collectionName = collectionName; } @@ -3265,10 +3264,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, maybeEmitEvent(new AfterLoadEvent<>(document, projection.getMappedType().getType(), collectionName)); - Object entity = reader.project(projection, document); + Object entity = mongoConverter.project(projection, document); if (entity == null) { - throw new MappingException(String.format("EntityReader %s returned null", reader)); + throw new MappingException(String.format("EntityReader %s returned null", mongoConverter)); } maybeEmitEvent(new AfterConvertEvent<>(document, entity, 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 eddad2ba4..22b3093e3 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 @@ -51,7 +51,7 @@ class PropertyOperations { Document computeMappedFieldsForProjection(EntityProjection projection, Document fields) { - if (!projection.isProjection() || !projection.isClosedProjection()) { + if (!projection.isClosedProjection()) { return fields; } 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 3cadc2c1b..b367ec9ef 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 @@ -2621,8 +2621,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati */ 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) { + FindAndReplaceOptions options, EntityProjection projection) { return Mono.defer(() -> { 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 540334a4d..9ab5de55a 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 @@ -32,6 +32,7 @@ import org.bson.BsonString; import org.bson.BsonValue; import org.bson.Document; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -44,6 +45,8 @@ import org.springframework.data.geo.Point; 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; @@ -549,6 +552,71 @@ class ExecutableFindOperationSupportTests { ).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 + @Disabled("GH-3913") + 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 @@ -618,6 +686,34 @@ class ExecutableFindOperationSupportTests { 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/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 7bf034f46..3227ba176 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -2050,6 +2050,8 @@ NOTE: Using projections allows `MongoTemplate` to optimize result mapping by lim by the projection target type. This applies as long as the `Query` itself does not contain any field restriction and the target type is a closed interface or DTO projection. +WARNING: Projections must not be applied to <>. + You can switch between retrieving a single entity and retrieving multiple entities as a `List` or a `Stream` through the terminating methods: `first()`, `one()`, `all()`, or `stream()`. When writing a geo-spatial query with `near(NearQuery)`, the number of terminating methods is altered to include only the methods that are valid for running a `geoNear` command in MongoDB (fetching entities as a `GeoResult` within `GeoResults`), as the following example shows: