From f902df9c75136a7c8bc75c512a4a28d0ef9c2b5d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 24 Nov 2021 15:30:40 +0100 Subject: [PATCH 1/8] Prepare issue branch. --- pom.xml | 4 ++-- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) 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 From 140c171191ecf00c095b4b532361b0ebb2dbdf22 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 24 Nov 2021 15:56:10 +0100 Subject: [PATCH 2/8] Hacking. --- .../core/convert/MappingMongoConverter.java | 191 ++++++++++++++++-- .../MappingMongoConverterUnitTests.java | 137 ++++++++++++- 2 files changed, 306 insertions(+), 22 deletions(-) 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..0aeba203d8 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 @@ -45,13 +45,16 @@ 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.Association; import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; 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.EntityProjectionIntrospector; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; @@ -75,6 +78,9 @@ 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.ProjectionInformation; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -114,6 +120,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App protected static final Log LOGGER = LogFactory.getLog(MappingMongoConverter.class); protected final MappingContext, MongoPersistentProperty> mappingContext; + private final ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); protected final QueryMapper idMapper; protected final DbRefResolver dbRefResolver; protected final DefaultDbRefProxyHandler dbRefProxyHandler; @@ -212,6 +219,10 @@ public MongoTypeMapper getTypeMapper() { return this.typeMapper; } + public ProjectionFactory getFactory() { + return factory; + } + /** * 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 @@ -276,6 +287,133 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) { this.entityCallbacks = entityCallbacks; } + public R project(EntityProjectionIntrospector.EntityProjection descriptor, Bson bson) { + + if (!descriptor.isProjection()) { // backed by real object + return read(descriptor.getMappedType(), bson); + } + + ProjectionInformation projectionInformation = factory + .getProjectionInformation(descriptor.getMappedType().getType()); + + if (!projectionInformation.isClosed()) { // backed by real object + return factory.createProjection(descriptor.getMappedType().getType(), read(descriptor.getMappedType(), bson)); + } + + ProjectingConversionContext context = new ProjectingConversionContext(conversions, ObjectPath.ROOT, + this::readCollectionOrArray, this::readMap, this::readDBRef, this::getPotentiallyConvertedSimpleRead, + descriptor); + + return doReadProjection(context, bson, descriptor); + } + + private R doReadProjection(ConversionContext context, Bson bson, + EntityProjectionIntrospector.EntityProjection descriptor) { + + MongoPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(descriptor.getActualDomainType()); + SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext); + DocumentAccessor documentAccessor = new DocumentAccessor(bson); + PersistentPropertyAccessor accessor; + + TypeInformation mappedType = descriptor.getActualMappedType(); + boolean isInterfaceProjection = mappedType.getType().isInterface(); + if (isInterfaceProjection) { + accessor = new MapPersistentPropertyAccessor(); + } else { + + // create target instance, use metadata from underlying domain type + MongoPersistentEntity entityToCreate = getMappingContext().getRequiredPersistentEntity(mappedType); + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); + ParameterValueProvider provider = persistenceConstructor != null + && persistenceConstructor.hasParameters() ? getParameterProvider(context, entity, documentAccessor, evaluator) + : NoOpParameterValueProvider.INSTANCE; + + EntityInstantiator instantiator = instantiators.getInstantiatorFor(entityToCreate); + Object instance = instantiator.createInstance(entityToCreate, provider); + + accessor = entityToCreate.getPropertyAccessor(instance); + } + + if (isInterfaceProjection || entity.requiresPropertyPopulation()) { + + PersistentPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor<>(accessor, conversionService); + MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, + evaluator); + readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator, false, + !isInterfaceProjection); + } + + if (isInterfaceProjection) { + return (R) factory.createProjection(mappedType.getType(), accessor.getBean()); + } + + return (R) accessor.getBean(); + } + + private Object doReadOrProject(ConversionContext context, Bson source, TypeInformation typeHint, + EntityProjectionIntrospector.EntityProjection typeDescriptor) { + + if (typeDescriptor.isProjection()) { + return doReadProjection(context, BsonUtils.asDocument(source), typeDescriptor); + } + + return readDocument(context, source, typeHint); + } + + class ProjectingConversionContext extends ConversionContext { + + private final EntityProjectionIntrospector.EntityProjection returnedTypeDescriptor; + + ProjectingConversionContext(CustomConversions customConversions, ObjectPath path, + ContainerValueConverter> collectionConverter, ContainerValueConverter mapConverter, + ContainerValueConverter dbRefConverter, ValueConverter elementConverter, + EntityProjectionIntrospector.EntityProjection returnedTypeDescriptor) { + super(customConversions, path, + (context, source, typeHint) -> doReadOrProject(context, source, typeHint, returnedTypeDescriptor), + + collectionConverter, mapConverter, dbRefConverter, elementConverter); + this.returnedTypeDescriptor = returnedTypeDescriptor; + } + + @Override + public ConversionContext forProperty(String name) { + + EntityProjectionIntrospector.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 +556,7 @@ private S populateProperties(ConversionContext context, MongoPersistentEntit MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(contextToUse, documentAccessor, evaluator); - readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator); + readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, true, true); return accessor.getBean(); } @@ -460,19 +598,24 @@ 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, boolean skipIdentifier, + boolean skipConstructorProperties) { DbRefResolverCallback callback = null; for (MongoPersistentProperty prop : entity) { + 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 +623,33 @@ private void readProperties(ConversionContext context, MongoPersistentEntity if (prop.isUnwrapped()) { accessor.setProperty(prop, - readUnwrapped(context, documentAccessor, prop, mappingContext.getRequiredPersistentEntity(prop))); + readUnwrapped(propertyContext, documentAccessor, prop, mappingContext.getRequiredPersistentEntity(prop))); continue; } // We skip the id property since it was already set - if (entity.isIdProperty(prop)) { + if (skipIdentifier && entity.isIdProperty(prop)) { continue; } - if (entity.isConstructorArgument(prop) || !documentAccessor.hasValue(prop)) { + if (skipConstructorProperties && (entity.isConstructorArgument(prop) || !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)); } } @@ -1773,6 +1917,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 +2144,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 +2246,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}. * 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..a3359a6ff3 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,7 @@ 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.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 +140,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 +2193,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 +2358,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 +2384,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 +2639,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.getFactory(), + EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), + mappingContext); + + EntityProjectionIntrospector.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.getFactory(), + EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), + mappingContext); + + EntityProjectionIntrospector.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.getFactory(), + EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), + mappingContext); + + EntityProjectionIntrospector.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.getFactory(), + EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), + mappingContext); + + EntityProjectionIntrospector.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 +2743,9 @@ interface InterfaceType { } @EqualsAndHashCode + @Getter static class Address implements InterfaceType { + @Field("s") String street; String city; } @@ -2696,6 +2775,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; } From a8c8537b747b2b4426f77157782de1d237295196 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 29 Nov 2021 12:13:35 +0100 Subject: [PATCH 3/8] Add general support for direct projections. TODO: Field annotations on projection types no longer considered, simple DTO's without accessors not considered DTO projections, causes conversion issues. --- .../data/mongodb/core/EntityOperations.java | 22 +++- .../data/mongodb/core/MongoTemplate.java | 104 ++++++++++----- .../data/mongodb/core/PropertyOperations.java | 46 ++----- .../data/mongodb/core/QueryOperations.java | 27 ++-- .../mongodb/core/ReactiveMongoTemplate.java | 118 ++++++++++------- .../core/convert/MappingMongoConverter.java | 122 ++++++++++++++---- .../mongodb/core/convert/MongoConverter.java | 28 ++++ .../support/MongoRepositoryFactory.java | 6 + .../ReactiveMongoRepositoryFactory.java | 6 + .../core/EntityOperationUnitTests.java | 4 +- .../core/EntityOperationsUnitTests.java | 6 +- .../ExecutableFindOperationSupportTests.java | 5 + ...ExecutableUpdateOperationSupportTests.java | 15 ++- .../core/MongoOperationsUnitTests.java | 18 +++ .../mongodb/core/MongoTemplateUnitTests.java | 10 +- .../core/ReactiveMongoTemplateUnitTests.java | 10 +- .../ReactiveUpdateOperationSupportTests.java | 10 +- .../core/UpdateOperationsUnitTests.java | 4 +- .../AbstractMongoConverterUnitTests.java | 18 +++ .../MappingMongoConverterUnitTests.java | 8 +- .../MongoRepositoryFactoryUnitTests.java | 5 + 21 files changed, 411 insertions(+), 181 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 3bba17aaef..3c7d713daa 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,16 @@ 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.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 +42,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 +67,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 +244,11 @@ public TypedOperations forType(@Nullable Class entityClass) { return UntypedOperations.instance(); } + public EntityProjectionIntrospector.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..23e2591534 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.EntityProjectionIntrospector; 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,9 +224,8 @@ 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.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext()); + this.operations = new EntityOperations(this.mongoConverter); + this.propertyOperations = new PropertyOperations(); 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); + EntityProjectionIntrospector.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); + EntityProjectionIntrospector.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); + EntityProjectionIntrospector.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, resultType, + 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, + EntityProjectionIntrospector.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, + EntityProjectionIntrospector.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); + EntityProjectionIntrospector.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,43 @@ 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) { + if (LOGGER.isDebugEnabled()) { + LOGGER + .debug(String.format( + "findAndReplace using query: %s fields: %s sort: %s for class: %s and replacement: %s " + + "in collection: %s", + serializeToJsonSafely(mappedQuery), serializeToJsonSafely(mappedFields), + serializeToJsonSafely(mappedSort), entityType, serializeToJsonSafely(replacement), collectionName)); + } + EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(resultType, + entityType); + + return executeFindOneInternal( + new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), + new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName); + } + + /** + * 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 resultType the target domain type. + * @return {@literal null} if object does not exist, {@link FindAndReplaceOptions#isReturnNew() return new} is + * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. + */ + @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, Class resultType, + EntityProjectionIntrospector.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 +2797,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 +3248,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 EntityProjectionIntrospector.EntityProjection projection; private final String collectionName; - ProjectingReadCallback(EntityReader reader, Class entityType, Class targetType, + ProjectingReadCallback(MongoConverter reader, EntityProjectionIntrospector.EntityProjection projection, String collectionName) { this.reader = reader; - this.entityType = entityType; - this.targetType = targetType; + this.projection = projection; this.collectionName = collectionName; } @@ -3230,21 +3271,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..298d4c09ae 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,63 +16,37 @@ package org.springframework.data.mongodb.core; import org.bson.Document; -import org.springframework.data.mapping.SimplePropertyHandler; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; -import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.projection.ProjectionInformation; -import org.springframework.util.ClassUtils; + +import org.springframework.data.mapping.context.EntityProjectionIntrospector; /** * 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 { - private final MappingContext, MongoPersistentProperty> mappingContext; - - PropertyOperations(MappingContext, MongoPersistentProperty> mappingContext) { - this.mappingContext = mappingContext; - } - /** * 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 computeFieldsForProjection(EntityProjectionIntrospector.EntityProjection projection, Document fields) { - if (!fields.isEmpty() || ClassUtils.isAssignable(domainType, targetType)) { + if (!fields.isEmpty() || !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)); - } - } else { - - MongoPersistentEntity entity = mappingContext.getPersistentEntity(targetType); - if (entity != null) { - entity.doWithProperties( - (SimplePropertyHandler) persistentProperty -> projectedFields.append(persistentProperty.getName(), 1)); - } - } + projection.forEach(propertyPath -> { + projectedFields.put(propertyPath.getSegment(), 1); + }); return projectedFields; } 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..43f73c2106 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.EntityProjectionIntrospector; 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,8 +288,8 @@ Document getMappedQuery(@Nullable MongoPersistentEntity entity) { return queryMapper.getMappedObject(getQueryObject(), entity); } - Document getMappedFields(@Nullable MongoPersistentEntity entity, Class targetType, - ProjectionFactory projectionFactory) { + Document getMappedFields(@Nullable MongoPersistentEntity entity, + EntityProjectionIntrospector.EntityProjection projection) { Document fields = new Document(); @@ -306,21 +306,12 @@ Document getMappedFields(@Nullable MongoPersistentEntity entity, Class tar } } - Document mappedFields = fields; - if (entity == null) { - return mappedFields; + return fields; } - Document projectedFields = propertyOperations.computeFieldsForProjection(projectionFactory, fields, - entity.getType(), targetType); - - if (ObjectUtils.nullSafeEquals(fields, projectedFields)) { - mappedFields = queryMapper.getMappedFields(projectedFields, entity); - } else { - mappedFields = queryMapper.getMappedFields(projectedFields, - mappingContext.getRequiredPersistentEntity(targetType)); - } + Document projectedFields = propertyOperations.computeFieldsForProjection(projection, fields); + Document mappedFields = queryMapper.getMappedFields(projectedFields, entity); if (entity.hasTextScoreProperty() && !query.getQueryObject().containsKey("$text")) { mappedFields.remove(entity.getTextScoreProperty().getFieldName()); @@ -388,8 +379,8 @@ private DistinctQueryContext(@Nullable Object query, String fieldName) { } @Override - Document getMappedFields(@Nullable MongoPersistentEntity entity, Class targetType, - ProjectionFactory projectionFactory) { + Document getMappedFields(@Nullable MongoPersistentEntity entity, + EntityProjectionIntrospector.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..9f4bf86b18 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.EntityProjectionIntrospector; 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.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); + EntityProjectionIntrospector.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); + EntityProjectionIntrospector.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, resultType, + 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, + EntityProjectionIntrospector.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, + EntityProjectionIntrospector.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); + EntityProjectionIntrospector.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) { @@ -2620,9 +2606,50 @@ protected Mono doFindAndReplace(String collectionName, Document mappedQue serializeToJsonSafely(replacement), collectionName)); } + EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(resultType, + entityType); + 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); + + }); + } + + /** + * 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 resultType the target domain type. + * @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 2.1 + */ + private Mono doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, + Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, + FindAndReplaceOptions options, Class resultType, + EntityProjectionIntrospector.EntityProjection projection) { + + return Mono.defer(() -> { + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format( + "findAndReplace using query: %s fields: %s sort: %s for class: %s and replacement: %s " + + "in collection: %s", + serializeToJsonSafely(mappedQuery), mappedFields, mappedSort, entityType, + serializeToJsonSafely(replacement), collectionName)); + } + + return executeFindOneInternal( + new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), + new ProjectingReadCallback<>(this.mongoConverter, projection, collectionName), collectionName); }); } @@ -3203,37 +3230,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 EntityProjectionIntrospector.EntityProjection projection; private final String collectionName; - ProjectingReadCallback(EntityReader reader, Class entityType, Class targetType, + ProjectingReadCallback(MongoConverter reader, EntityProjectionIntrospector.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; + Class returnType = projection.getMappedType().getType(); + maybeEmitEvent(new AfterLoadEvent<>(document, returnType, collectionName)); - maybeEmitEvent(new AfterLoadEvent<>(document, typeToRead, collectionName)); - - Object entity = reader.read(typeToRead, document); + Object entity = reader.project(projection, document); if (entity == null) { throw new MappingException(String.format("EntityReader %s returned null", reader)); } - Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, entity) : entity; - - T castEntity = (T) result; + T castEntity = (T) entity; maybeEmitEvent(new AfterConvertEvent<>(document, castEntity, collectionName)); return maybeCallAfterConvert(castEntity, document, collectionName); } 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 0aeba203d8..62eb423132 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; @@ -47,10 +48,14 @@ 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; @@ -79,7 +84,6 @@ 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.ProjectionInformation; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; @@ -120,7 +124,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App protected static final Log LOGGER = LogFactory.getLog(MappingMongoConverter.class); protected final MappingContext, MongoPersistentProperty> mappingContext; - private final ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); protected final QueryMapper idMapper; protected final DbRefResolver dbRefResolver; protected final DefaultDbRefProxyHandler dbRefProxyHandler; @@ -134,6 +137,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App private SpELContext spELContext; private @Nullable EntityCallbacks entityCallbacks; private final DocumentPointerFactory documentPointerFactory; + private final ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); /** * Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}. @@ -219,10 +223,16 @@ public MongoTypeMapper getTypeMapper() { return this.typeMapper; } - public ProjectionFactory getFactory() { + @Override + public ProjectionFactory getProjectionFactory() { return factory; } + @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 @@ -287,19 +297,13 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) { this.entityCallbacks = entityCallbacks; } + @Override public R project(EntityProjectionIntrospector.EntityProjection descriptor, Bson bson) { if (!descriptor.isProjection()) { // backed by real object return read(descriptor.getMappedType(), bson); } - ProjectionInformation projectionInformation = factory - .getProjectionInformation(descriptor.getMappedType().getType()); - - if (!projectionInformation.isClosed()) { // backed by real object - return factory.createProjection(descriptor.getMappedType().getType(), read(descriptor.getMappedType(), bson)); - } - ProjectingConversionContext context = new ProjectingConversionContext(conversions, ObjectPath.ROOT, this::readCollectionOrArray, this::readMap, this::readDBRef, this::getPotentiallyConvertedSimpleRead, descriptor); @@ -336,11 +340,21 @@ private R doReadProjection(ConversionContext context, Bson bson, if (isInterfaceProjection || entity.requiresPropertyPopulation()) { - PersistentPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor<>(accessor, conversionService); + MongoPersistentEntity mappedEntity = getMappingContext().getPersistentEntity(mappedType); + PersistentPropertyAccessor convertingAccessor = PropertyTranslatingPropertyAccessor + .create(new ConvertingPropertyAccessor<>(accessor, conversionService), mappedEntity); MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, evaluator); - readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator, false, - !isInterfaceProjection); + + Predicate propertyFilter; + + if (isInterfaceProjection) { + propertyFilter = it -> true; + } else { + propertyFilter = it -> mappedEntity != null && mappedEntity.getPersistentProperty(it.getName()) != null; + } + + readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator, propertyFilter); } if (isInterfaceProjection) { @@ -556,7 +570,8 @@ private S populateProperties(ConversionContext context, MongoPersistentEntit MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(contextToUse, documentAccessor, evaluator); - readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, true, true); + Predicate propertyFilter = isIdentifier(entity).or(isConstructorArgument(entity)).negate(); + readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, propertyFilter); return accessor.getBean(); } @@ -598,13 +613,17 @@ private Object readIdValue(ConversionContext context, SpELExpressionEvaluator ev private void readProperties(ConversionContext context, MongoPersistentEntity entity, PersistentPropertyAccessor accessor, DocumentAccessor documentAccessor, - MongoDbPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator, boolean skipIdentifier, - boolean skipConstructorProperties) { + 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); @@ -627,13 +646,7 @@ private void readProperties(ConversionContext context, MongoPersistentEntity continue; } - // We skip the id property since it was already set - - if (skipIdentifier && entity.isIdProperty(prop)) { - continue; - } - - if (skipConstructorProperties && (entity.isConstructorArgument(prop) || !documentAccessor.hasValue(prop))) { + if (!documentAccessor.hasValue(prop)) { continue; } @@ -1854,6 +1867,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}. @@ -2274,4 +2295,59 @@ interface ContainerValueConverter { } } + + private static class PropertyTranslatingPropertyAccessor implements PersistentPropertyPathAccessor { + + private final PersistentPropertyAccessor delegate; + private final MongoPersistentEntity mappedEntity; + + private PropertyTranslatingPropertyAccessor(PersistentPropertyAccessor delegate, + MongoPersistentEntity mappedEntity) { + this.delegate = delegate; + this.mappedEntity = mappedEntity; + } + + static PersistentPropertyAccessor create(PersistentPropertyAccessor delegate, + @Nullable MongoPersistentEntity mappedEntity) { + return mappedEntity == null ? delegate : new PropertyTranslatingPropertyAccessor<>(delegate, mappedEntity); + } + + @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 PersistentProperty translate(PersistentProperty property) { + + MongoPersistentProperty translated = mappedEntity.getPersistentProperty(property.getName()); + return translated != null ? translated : 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..30c85937d3 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 @@ -20,12 +20,15 @@ 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.EntityProjectionIntrospector; 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 +57,30 @@ public interface MongoConverter */ MongoTypeMapper getTypeMapper(); + /** + * Returns the {@link ProjectionFactory} for this converter. + * + * @return + */ + ProjectionFactory getProjectionFactory(); + + /** + * Returns the {@link CustomConversions} for this converter. + * + * @return + */ + CustomConversions getCustomConversions(); + + /** + * Apply a projection to {@link Bson} and return the projection return type {@code R}. + * + * @param descriptor + * @param bson + * @param + * @return + */ + R project(EntityProjectionIntrospector.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 +181,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/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..540334a4d3 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; @@ -569,6 +572,8 @@ public interface PersonSpELProjection { String getName(); } + @Getter + @Setter // TODO: Without getters/setters, not identified as projection/properties static class PersonDtoProjection { @Field("firstname") String name; 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..1e43a2d9a2 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 @@ -25,6 +25,7 @@ import org.bson.BsonString; 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; import org.springframework.data.annotation.Id; @@ -132,10 +133,11 @@ void updateAllMatchingCriteria() { } @Test // DATAMONGO-1563 + @Disabled("TODO") void updateWithDifferentDomainClassAndCollection() { UpdateResult result = template.update(Jedi.class).inCollection(STAR_WARS) - .matching(query(where("_id").is(han.getId()))).apply(new Update().set("name", "Han")).all(); + .matching(query(where("_id").is(han.getId()))).apply(new Update().set("firstname", "Han")).all(); assertThat(result.getModifiedCount()).isEqualTo(1L); assertThat(result.getUpsertedId()).isNull(); @@ -166,12 +168,13 @@ void findAndModify() { } @Test // DATAMONGO-1563 + @Disabled("TODO") void findAndModifyWithDifferentDomainTypeAndCollection() { Optional result = template.update(Jedi.class).inCollection(STAR_WARS) - .matching(query(where("_id").is(han.getId()))).apply(new Update().set("name", "Han")).findAndModify(); + .matching(query(where("_id").is(han.getId()))).apply(new Update().set("firstname", "Han")).findAndModify(); - assertThat(result.get()).hasFieldOrPropertyWithValue("name", "han"); + assertThat(result.get()).hasFieldOrPropertyWithValue("firstname", "han"); assertThat(template.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); } @@ -257,7 +260,7 @@ void findAndReplaceWithProjection() { Jedi result = template.update(Person.class).matching(queryHan()).replaceWith(luke).as(Jedi.class) .findAndReplaceValue(); - assertThat(result.getName()).isEqualTo(han.firstname); + assertThat(result.getFirstname()).isEqualTo(han.firstname); } private Query queryHan() { @@ -268,6 +271,7 @@ private Query queryHan() { @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) static class Person { @Id String id; + @Field("name") String firstname; } @@ -278,7 +282,6 @@ static class Human { @Data static class Jedi { - - @Field("firstname") String name; + String firstname; } } 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..2e4a1ca96d 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 @@ -28,8 +28,10 @@ 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.EntityProjectionIntrospector; 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 +39,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 +95,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(EntityProjectionIntrospector.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..5565aefa75 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)) @@ -1078,7 +1080,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { @Test // DATAMONGO-1733, DATAMONGO-2041 void appliesFieldsToDtoProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, + template.doFind("star-wars", new Document(), new Document(), Person.class, PersonDtoProjection.class, CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("firstname", 1))); @@ -2359,6 +2361,12 @@ static class Jedi { @Field("firstname") String name; } + @Data + static class PersonDtoProjection { + + String firstname; + } + class Wrapper { AutogenerateableId foo; 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..d5b671d67e 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; @@ -408,7 +409,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { @Test // DATAMONGO-1719, DATAMONGO-2041 void appliesFieldsToDtoProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, + template.doFind("star-wars", new Document(), new Document(), Person.class, PersonDtoProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("firstname", 1))); @@ -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); @@ -1502,6 +1504,12 @@ static class Jedi { @Field("firstname") String name; } + @Data + static class PersonDtoProjection { + + String firstname; + } + @org.springframework.data.mongodb.core.mapping.Document(collation = "de_AT") static class Sith { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java index 587380bc81..d9b9a879c6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java @@ -24,8 +24,10 @@ import org.bson.BsonString; 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; + import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Query; @@ -164,11 +166,12 @@ void findAndModify() { } @Test // DATAMONGO-1719 + @Disabled("TODO") void findAndModifyWithDifferentDomainTypeAndCollection() { template.update(Jedi.class).inCollection(STAR_WARS).matching(query(where("_id").is(han.getId()))) .apply(new Update().set("name", "Han")).findAndModify().as(StepVerifier::create) - .consumeNextWith(actual -> assertThat(actual.getName()).isEqualTo("han")).verifyComplete(); + .consumeNextWith(actual -> assertThat(actual.getFirstname()).isEqualTo("han")).verifyComplete(); assertThat(blocking.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); @@ -220,7 +223,7 @@ void findAndReplaceWithProjection() { template.update(Person.class).matching(queryHan()).replaceWith(luke).as(Jedi.class).findAndReplace() // .as(StepVerifier::create).consumeNextWith(it -> { - assertThat(it.getName()).isEqualTo(han.firstname); + assertThat(it.getFirstname()).isEqualTo(han.firstname); }).verifyComplete(); } @@ -262,6 +265,7 @@ private Query queryHan() { @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) static class Person { @Id String id; + @Field("name") String firstname; } @@ -273,6 +277,6 @@ static class Human { @Data static class Jedi { - @Field("firstname") String name; + String firstname; } } 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..2db2d38b2f 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(); 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..c3c32aafe0 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 @@ -21,11 +21,14 @@ 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.EntityProjectionIntrospector; 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 +62,21 @@ public MongoTypeMapper getTypeMapper() { throw new UnsupportedOperationException(); } + @Override + public ProjectionFactory getProjectionFactory() { + return null; + } + + @Override + public CustomConversions getCustomConversions() { + return null; + } + + @Override + public R project(EntityProjectionIntrospector.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 a3359a6ff3..c543ad790e 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 @@ -2645,7 +2645,7 @@ 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.getFactory(), + EntityProjectionIntrospector discoverer = EntityProjectionIntrospector.create(converter.getProjectionFactory(), EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), mappingContext); @@ -2664,7 +2664,7 @@ 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.getFactory(), + EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(), EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), mappingContext); @@ -2683,7 +2683,7 @@ void projectShouldReadNestedProjection() { org.bson.Document source = new org.bson.Document("addresses", Collections.singletonList(new org.bson.Document("s", "hwy"))); - EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getFactory(), + EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(), EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), mappingContext); @@ -2701,7 +2701,7 @@ void projectShouldReadProjectionWithNestedEntity() { org.bson.Document source = new org.bson.Document("addresses", Collections.singletonList(new org.bson.Document("s", "hwy"))); - EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getFactory(), + EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(), EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), mappingContext); 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 From 808b08dd22d148d81c960be727f2feb4f653031e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 30 Nov 2021 11:14:04 +0100 Subject: [PATCH 4/8] Polishing. Consider merged mapping metadata for DTO projections that may shadow entity properties or may contain additional Field annotations. --- .../data/mongodb/core/MongoTemplate.java | 22 ++-- .../data/mongodb/core/PropertyOperations.java | 36 +++++- .../data/mongodb/core/QueryOperations.java | 54 ++++++--- .../mongodb/core/ReactiveMongoTemplate.java | 29 ++--- .../core/convert/DocumentAccessor.java | 10 +- .../core/convert/MappingMongoConverter.java | 113 ++++++++++-------- .../mongodb/core/convert/MongoConverter.java | 17 ++- .../mongodb/core/convert/QueryMapper.java | 2 +- .../mapping/BasicMongoPersistentProperty.java | 2 +- .../core/mapping/MongoPersistentProperty.java | 7 ++ .../mapping/PersistentPropertyTranslator.java | 92 ++++++++++++++ .../UnwrappedMongoPersistentProperty.java | 6 + ...ExecutableUpdateOperationSupportTests.java | 15 +-- .../mongodb/core/MongoTemplateUnitTests.java | 8 +- .../core/ReactiveMongoTemplateUnitTests.java | 9 +- .../ReactiveUpdateOperationSupportTests.java | 10 +- .../core/UpdateOperationsUnitTests.java | 2 +- 17 files changed, 283 insertions(+), 151 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/PersistentPropertyTranslator.java 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 23e2591534..b3279e73fb 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 @@ -225,7 +225,7 @@ public MongoTemplate(MongoDatabaseFactory mongoDbFactory, @Nullable MongoConvert this.updateMapper = new UpdateMapper(this.mongoConverter); this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter); this.operations = new EntityOperations(this.mongoConverter); - this.propertyOperations = new PropertyOperations(); + this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext()); this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations, mongoDbFactory); @@ -1061,7 +1061,7 @@ 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) { @@ -2751,20 +2751,11 @@ 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) { - if (LOGGER.isDebugEnabled()) { - LOGGER - .debug(String.format( - "findAndReplace using query: %s fields: %s sort: %s for class: %s and replacement: %s " - + "in collection: %s", - serializeToJsonSafely(mappedQuery), serializeToJsonSafely(mappedFields), - serializeToJsonSafely(mappedSort), entityType, serializeToJsonSafely(replacement), collectionName)); - } EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(resultType, entityType); - return executeFindOneInternal( - new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), - new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName); + return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement, + options, projection); } /** @@ -2778,14 +2769,15 @@ protected T doFindAndReplace(String collectionName, Document mappedQuery, Do * @param entityType the source domain type. * @param replacement the replacement {@link Document}. * @param options applicable options. - * @param resultType the target domain type. + * @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 2.7 */ @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, Class resultType, + Document replacement, FindAndReplaceOptions options, EntityProjectionIntrospector.EntityProjection projection) { if (LOGGER.isDebugEnabled()) { 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 298d4c09ae..c13bed4833 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 @@ -18,6 +18,11 @@ import org.bson.Document; import org.springframework.data.mapping.context.EntityProjectionIntrospector; +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.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. @@ -28,6 +33,12 @@ */ class PropertyOperations { + private final MappingContext, MongoPersistentProperty> mappingContext; + + PropertyOperations(MappingContext, MongoPersistentProperty> mappingContext) { + this.mappingContext = mappingContext; + } + /** * 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 EntityProjection} is a {@literal DTO projection} or a @@ -37,16 +48,31 @@ class PropertyOperations { * @param fields must not be {@literal null}. * @return {@link Document} with fields to be included. */ - Document computeFieldsForProjection(EntityProjectionIntrospector.EntityProjection projection, Document fields) { + Document computeMappedFieldsForProjection(EntityProjectionIntrospector.EntityProjection projection, + Document fields) { - if (!fields.isEmpty() || !projection.isProjection() || !projection.isClosedProjection()) { + if (!projection.isProjection() || !projection.isClosedProjection()) { return fields; } Document projectedFields = new Document(); - projection.forEach(propertyPath -> { - projectedFields.put(propertyPath.getSegment(), 1); - }); + + if (projection.getMappedType().getType().isInterface()) { + + projection.forEach(propertyPath -> projectedFields.put(propertyPath.getSegment(), 1)); + } else { + + // 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); + } + } return projectedFields; } 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 43f73c2106..70eadc0ad4 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 @@ -291,33 +291,55 @@ Document getMappedQuery(@Nullable MongoPersistentEntity entity) { Document getMappedFields(@Nullable MongoPersistentEntity entity, EntityProjectionIntrospector.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; } - if (entity == null) { - return fields; + return mappedFields; + } + + private Document evaluateFields(@Nullable MongoPersistentEntity entity) { + + Document fields = query.getFieldsObject(); + + if (fields.isEmpty()) { + return BsonUtils.EMPTY_DOCUMENT; } - Document projectedFields = propertyOperations.computeFieldsForProjection(projection, fields); - Document mappedFields = queryMapper.getMappedFields(projectedFields, entity); + Document evaluated = new Document(); - if (entity.hasTextScoreProperty() && !query.getQueryObject().containsKey("$text")) { - mappedFields.remove(entity.getTextScoreProperty().getFieldName()); + for (Entry entry : fields.entrySet()) { + + if (entry.getValue() instanceof MongoExpression) { + + AggregationOperationContext ctx = entity == null ? Aggregation.DEFAULT_CONTEXT + : new RelaxedTypeBasedAggregationOperationContext(entity.getType(), mappingContext, queryMapper); + + evaluated.put(entry.getKey(), AggregationExpression.from((MongoExpression) entry.getValue()).toDocument(ctx)); + } else { + evaluated.put(entry.getKey(), entry.getValue()); + } } - return mappedFields; + return evaluated; } /** 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 9f4bf86b18..590d700d66 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 @@ -246,7 +246,7 @@ public ReactiveMongoTemplate(ReactiveMongoDatabaseFactory mongoDatabaseFactory, // 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.mongoConverter); - this.propertyOperations = new PropertyOperations(); + this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext()); this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations, mongoDatabaseFactory); @@ -1159,7 +1159,7 @@ 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())); @@ -2596,24 +2596,11 @@ protected Mono doFindAndReplace(String collectionName, Document mappedQue Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { - return Mono.defer(() -> { - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format( - "findAndReplace using query: %s fields: %s sort: %s for class: %s and replacement: %s " - + "in collection: %s", - serializeToJsonSafely(mappedQuery), mappedFields, mappedSort, entityType, - serializeToJsonSafely(replacement), collectionName)); - } - - EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(resultType, + EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(resultType, entityType); - return executeFindOneInternal( - new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), - new ProjectingReadCallback<>(this.mongoConverter, projection, collectionName), collectionName); - - }); + return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement, + options, projection); } /** @@ -2627,14 +2614,14 @@ protected Mono doFindAndReplace(String collectionName, Document mappedQue * @param entityType the source domain type. * @param replacement the replacement {@link Document}. * @param options applicable options. - * @param resultType the target domain type. + * @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 2.1 + * @since 2.7 */ private Mono doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, - FindAndReplaceOptions options, Class resultType, + FindAndReplaceOptions options, EntityProjectionIntrospector.EntityProjection projection) { return Mono.defer(() -> { 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 62eb423132..5479d717c2 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 @@ -76,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; @@ -86,6 +87,7 @@ 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; @@ -298,70 +300,80 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) { } @Override - public R project(EntityProjectionIntrospector.EntityProjection descriptor, Bson bson) { + public R project(EntityProjectionIntrospector.EntityProjection projection, Bson bson) { - if (!descriptor.isProjection()) { // backed by real object - return read(descriptor.getMappedType(), bson); + if (!projection.isProjection()) { // backed by real object + return read(projection.getMappedType(), bson); } ProjectingConversionContext context = new ProjectingConversionContext(conversions, ObjectPath.ROOT, this::readCollectionOrArray, this::readMap, this::readDBRef, this::getPotentiallyConvertedSimpleRead, - descriptor); + projection); - return doReadProjection(context, bson, descriptor); + return doReadProjection(context, bson, projection); } + @SuppressWarnings("unchecked") private R doReadProjection(ConversionContext context, Bson bson, - EntityProjectionIntrospector.EntityProjection descriptor) { + EntityProjectionIntrospector.EntityProjection projection) { - MongoPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(descriptor.getActualDomainType()); + MongoPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(projection.getActualDomainType()); + TypeInformation mappedType = projection.getActualMappedType(); + MongoPersistentEntity mappedEntity = (MongoPersistentEntity) getMappingContext() + .getPersistentEntity(mappedType); SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext); - DocumentAccessor documentAccessor = new DocumentAccessor(bson); - PersistentPropertyAccessor accessor; - TypeInformation mappedType = descriptor.getActualMappedType(); boolean isInterfaceProjection = mappedType.getType().isInterface(); if (isInterfaceProjection) { - accessor = new MapPersistentPropertyAccessor(); - } else { - - // create target instance, use metadata from underlying domain type - MongoPersistentEntity entityToCreate = getMappingContext().getRequiredPersistentEntity(mappedType); - PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); - ParameterValueProvider provider = persistenceConstructor != null - && persistenceConstructor.hasParameters() ? getParameterProvider(context, entity, documentAccessor, evaluator) - : NoOpParameterValueProvider.INSTANCE; - - EntityInstantiator instantiator = instantiators.getInstantiatorFor(entityToCreate); - Object instance = instantiator.createInstance(entityToCreate, provider); - - accessor = entityToCreate.getPropertyAccessor(instance); - } - if (isInterfaceProjection || entity.requiresPropertyPopulation()) { + PersistentPropertyTranslator propertyTranslator = PersistentPropertyTranslator.create(mappedEntity); + DocumentAccessor documentAccessor = new DocumentAccessor(bson); + PersistentPropertyAccessor accessor = new MapPersistentPropertyAccessor(); - MongoPersistentEntity mappedEntity = getMappingContext().getPersistentEntity(mappedType); PersistentPropertyAccessor convertingAccessor = PropertyTranslatingPropertyAccessor - .create(new ConvertingPropertyAccessor<>(accessor, conversionService), mappedEntity); + .create(new ConvertingPropertyAccessor<>(accessor, conversionService), propertyTranslator); MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, evaluator); - Predicate propertyFilter; + readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator, + Predicates.isTrue()); + return (R) factory.createProjection(mappedType.getType(), accessor.getBean()); + } - if (isInterfaceProjection) { - propertyFilter = it -> true; - } else { - propertyFilter = it -> mappedEntity != null && mappedEntity.getPersistentProperty(it.getName()) != null; + // 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(); } + }; - readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator, propertyFilter); - } + PreferredConstructor persistenceConstructor = mappedEntity.getPersistenceConstructor(); + ParameterValueProvider provider = persistenceConstructor != null + && persistenceConstructor.hasParameters() + ? getParameterProvider(context, mappedEntity, documentAccessor, evaluator) + : NoOpParameterValueProvider.INSTANCE; - if (isInterfaceProjection) { - return (R) factory.createProjection(mappedType.getType(), accessor.getBean()); - } + EntityInstantiator instantiator = instantiators.getInstantiatorFor(mappedEntity); + R instance = instantiator.createInstance(mappedEntity, provider); + PersistentPropertyAccessor accessor = mappedEntity.getPropertyAccessor(instance); + + populateProperties(context, mappedEntity, documentAccessor, evaluator, instance); - return (R) accessor.getBean(); + 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, @@ -381,12 +393,12 @@ class ProjectingConversionContext extends ConversionContext { ProjectingConversionContext(CustomConversions customConversions, ObjectPath path, ContainerValueConverter> collectionConverter, ContainerValueConverter mapConverter, ContainerValueConverter dbRefConverter, ValueConverter elementConverter, - EntityProjectionIntrospector.EntityProjection returnedTypeDescriptor) { + EntityProjectionIntrospector.EntityProjection projection) { super(customConversions, path, - (context, source, typeHint) -> doReadOrProject(context, source, typeHint, returnedTypeDescriptor), + (context, source, typeHint) -> doReadOrProject(context, source, typeHint, projection), collectionConverter, mapConverter, dbRefConverter, elementConverter); - this.returnedTypeDescriptor = returnedTypeDescriptor; + this.returnedTypeDescriptor = projection; } @Override @@ -2299,17 +2311,17 @@ interface ContainerValueConverter { private static class PropertyTranslatingPropertyAccessor implements PersistentPropertyPathAccessor { private final PersistentPropertyAccessor delegate; - private final MongoPersistentEntity mappedEntity; + private final PersistentPropertyTranslator propertyTranslator; private PropertyTranslatingPropertyAccessor(PersistentPropertyAccessor delegate, - MongoPersistentEntity mappedEntity) { + PersistentPropertyTranslator propertyTranslator) { this.delegate = delegate; - this.mappedEntity = mappedEntity; + this.propertyTranslator = propertyTranslator; } static PersistentPropertyAccessor create(PersistentPropertyAccessor delegate, - @Nullable MongoPersistentEntity mappedEntity) { - return mappedEntity == null ? delegate : new PropertyTranslatingPropertyAccessor<>(delegate, mappedEntity); + PersistentPropertyTranslator propertyTranslator) { + return new PropertyTranslatingPropertyAccessor<>(delegate, propertyTranslator); } @Override @@ -2344,10 +2356,9 @@ public void setProperty(PersistentPropertyPath> throw new UnsupportedOperationException(); } - private PersistentProperty translate(PersistentProperty property) { - - MongoPersistentProperty translated = mappedEntity.getPersistentProperty(property.getName()); - return translated != null ? translated : property; + 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 30c85937d3..b6cd10a687 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 @@ -60,24 +60,29 @@ public interface MongoConverter /** * Returns the {@link ProjectionFactory} for this converter. * - * @return + * @return will never be {@literal null}. + * @since 2.7 */ ProjectionFactory getProjectionFactory(); /** * Returns the {@link CustomConversions} for this converter. * - * @return + * @return will never be {@literal null}. + * @since 2.7 */ CustomConversions getCustomConversions(); /** - * Apply a projection to {@link Bson} and return the projection return type {@code R}. + * Apply a projection to {@link Bson} and return the projection return type {@code R}. + * {@link EntityProjectionIntrospector.EntityProjection#isProjection() Non-projecting} descriptors fall back to + * {@link #read(Class, Object) regular object materialization}. * - * @param descriptor - * @param bson + * @param descriptor the projection descriptor, must not be {@literal null}. + * @param bson must not be {@literal null}. * @param - * @return + * @return a new instance of the projection return type {@code R}. + * @since 2.7 */ R project(EntityProjectionIntrospector.EntityProjection descriptor, Bson bson); 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 2.7 + */ +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/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java index 1e43a2d9a2..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 @@ -25,9 +25,9 @@ import org.bson.BsonString; 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; + import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Query; @@ -133,11 +133,10 @@ void updateAllMatchingCriteria() { } @Test // DATAMONGO-1563 - @Disabled("TODO") void updateWithDifferentDomainClassAndCollection() { UpdateResult result = template.update(Jedi.class).inCollection(STAR_WARS) - .matching(query(where("_id").is(han.getId()))).apply(new Update().set("firstname", "Han")).all(); + .matching(query(where("_id").is(han.getId()))).apply(new Update().set("name", "Han")).all(); assertThat(result.getModifiedCount()).isEqualTo(1L); assertThat(result.getUpsertedId()).isNull(); @@ -168,13 +167,12 @@ void findAndModify() { } @Test // DATAMONGO-1563 - @Disabled("TODO") void findAndModifyWithDifferentDomainTypeAndCollection() { Optional result = template.update(Jedi.class).inCollection(STAR_WARS) - .matching(query(where("_id").is(han.getId()))).apply(new Update().set("firstname", "Han")).findAndModify(); + .matching(query(where("_id").is(han.getId()))).apply(new Update().set("name", "Han")).findAndModify(); - assertThat(result.get()).hasFieldOrPropertyWithValue("firstname", "han"); + assertThat(result.get()).hasFieldOrPropertyWithValue("name", "han"); assertThat(template.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); } @@ -260,7 +258,7 @@ void findAndReplaceWithProjection() { Jedi result = template.update(Person.class).matching(queryHan()).replaceWith(luke).as(Jedi.class) .findAndReplaceValue(); - assertThat(result.getFirstname()).isEqualTo(han.firstname); + assertThat(result.getName()).isEqualTo(han.firstname); } private Query queryHan() { @@ -271,7 +269,6 @@ private Query queryHan() { @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) static class Person { @Id String id; - @Field("name") String firstname; } @@ -282,6 +279,6 @@ static class Human { @Data static class Jedi { - String firstname; + @Field("firstname") String name; } } 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 5565aefa75..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 @@ -1080,7 +1080,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { @Test // DATAMONGO-1733, DATAMONGO-2041 void appliesFieldsToDtoProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonDtoProjection.class, + template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("firstname", 1))); @@ -2361,12 +2361,6 @@ static class Jedi { @Field("firstname") String name; } - @Data - static class PersonDtoProjection { - - String firstname; - } - class Wrapper { AutogenerateableId foo; 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 d5b671d67e..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 @@ -409,7 +409,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { @Test // DATAMONGO-1719, DATAMONGO-2041 void appliesFieldsToDtoProjection() { - template.doFind("star-wars", new Document(), new Document(), Person.class, PersonDtoProjection.class, + template.doFind("star-wars", new Document(), new Document(), Person.class, Jedi.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("firstname", 1))); @@ -1482,7 +1482,6 @@ class Wrapper { AutogenerateableId foo; } - static class PersonExtended extends Person { String lastname; @@ -1504,12 +1503,6 @@ static class Jedi { @Field("firstname") String name; } - @Data - static class PersonDtoProjection { - - String firstname; - } - @org.springframework.data.mongodb.core.mapping.Document(collation = "de_AT") static class Sith { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java index d9b9a879c6..587380bc81 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java @@ -24,10 +24,8 @@ import org.bson.BsonString; 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; - import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Query; @@ -166,12 +164,11 @@ void findAndModify() { } @Test // DATAMONGO-1719 - @Disabled("TODO") void findAndModifyWithDifferentDomainTypeAndCollection() { template.update(Jedi.class).inCollection(STAR_WARS).matching(query(where("_id").is(han.getId()))) .apply(new Update().set("name", "Han")).findAndModify().as(StepVerifier::create) - .consumeNextWith(actual -> assertThat(actual.getFirstname()).isEqualTo("han")).verifyComplete(); + .consumeNextWith(actual -> assertThat(actual.getName()).isEqualTo("han")).verifyComplete(); assertThat(blocking.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); @@ -223,7 +220,7 @@ void findAndReplaceWithProjection() { template.update(Person.class).matching(queryHan()).replaceWith(luke).as(Jedi.class).findAndReplace() // .as(StepVerifier::create).consumeNextWith(it -> { - assertThat(it.getFirstname()).isEqualTo(han.firstname); + assertThat(it.getName()).isEqualTo(han.firstname); }).verifyComplete(); } @@ -265,7 +262,6 @@ private Query queryHan() { @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) static class Person { @Id String id; - @Field("name") String firstname; } @@ -277,6 +273,6 @@ static class Human { @Data static class Jedi { - String firstname; + @Field("firstname") String name; } } 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 2db2d38b2f..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 @@ -49,7 +49,7 @@ class UpdateOperationsUnitTests { QueryMapper queryMapper = new QueryMapper(mongoConverter); UpdateMapper updateMapper = new UpdateMapper(mongoConverter); EntityOperations entityOperations = new EntityOperations(mongoConverter); - PropertyOperations propertyOperations = new PropertyOperations(); + PropertyOperations propertyOperations = new PropertyOperations(mongoConverter.getMappingContext()); ExtendedQueryOperations queryOperations = new ExtendedQueryOperations(queryMapper, updateMapper, entityOperations, propertyOperations, MongoClientSettings::getDefaultCodecRegistry); From 6b820fa800f84c080c19b12ba01144b062adad6f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 30 Nov 2021 11:36:02 +0100 Subject: [PATCH 5/8] Adopt to changes in Commons. --- .../data/mongodb/core/EntityOperations.java | 3 ++- .../data/mongodb/core/MongoTemplate.java | 22 +++++++++---------- .../data/mongodb/core/PropertyOperations.java | 4 ++-- .../data/mongodb/core/QueryOperations.java | 6 ++--- .../mongodb/core/ReactiveMongoTemplate.java | 20 ++++++++--------- .../core/convert/MappingMongoConverter.java | 14 ++++++------ .../mongodb/core/convert/MongoConverter.java | 9 ++++---- .../core/MongoOperationsUnitTests.java | 5 +++-- .../AbstractMongoConverterUnitTests.java | 5 +++-- .../MappingMongoConverterUnitTests.java | 9 ++++---- 10 files changed, 51 insertions(+), 46 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 3c7d713daa..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 @@ -28,6 +28,7 @@ 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; @@ -244,7 +245,7 @@ public TypedOperations forType(@Nullable Class entityClass) { return UntypedOperations.instance(); } - public EntityProjectionIntrospector.EntityProjection introspectProjection(Class resultType, + 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 b3279e73fb..3feb4ba06b 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,7 +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.EntityProjectionIntrospector; +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; @@ -410,7 +410,7 @@ protected CloseableIterator doStream(Query query, Class entityType, St MongoPersistentEntity persistentEntity = mappingContext.getPersistentEntity(entityType); QueryContext queryContext = queryOperations.createQueryContext(query); - EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(returnType, + EntityProjection projection = operations.introspectProjection(returnType, entityType); Document mappedQuery = queryContext.getMappedQuery(persistentEntity); @@ -960,7 +960,7 @@ public GeoResults geoNear(NearQuery near, Class domainType, String col .withOptions(AggregationOptions.builder().collation(near.getCollation()).build()); AggregationResults results = aggregate($geoNear, collection, Document.class); - EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(returnType, + EntityProjection projection = operations.introspectProjection(returnType, domainType); DocumentCallback> callback = new GeoNearResultDocumentCallback<>(distanceField, @@ -1048,7 +1048,7 @@ public T findAndReplace(Query query, S replacement, FindAndReplaceOptions MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityType); QueryContext queryContext = queryOperations.createQueryContext(query); - EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(resultType, + EntityProjection projection = operations.introspectProjection(resultType, entityType); Document mappedQuery = queryContext.getMappedQuery(entity); Document mappedFields = queryContext.getMappedFields(entity, projection); @@ -2501,7 +2501,7 @@ protected T doFindOne(String collectionName, Document query, Document fields QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); Document mappedFields = queryContext.getMappedFields(entity, - EntityProjectionIntrospector.EntityProjection.nonProjecting(entityClass)); + EntityProjection.nonProjecting(entityClass)); Document mappedQuery = queryContext.getMappedQuery(entity); if (LOGGER.isDebugEnabled()) { @@ -2554,7 +2554,7 @@ protected List doFind(String collectionName, Document query, Document QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); Document mappedFields = queryContext.getMappedFields(entity, - EntityProjectionIntrospector.EntityProjection.nonProjecting(entityClass)); + EntityProjection.nonProjecting(entityClass)); Document mappedQuery = queryContext.getMappedQuery(entity); if (LOGGER.isDebugEnabled()) { @@ -2576,7 +2576,7 @@ List doFind(String collectionName, Document query, Document fields, Cl Class targetClass, CursorPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); - EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(targetClass, + EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); @@ -2751,7 +2751,7 @@ 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) { - EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(resultType, + EntityProjection projection = operations.introspectProjection(resultType, entityType); return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement, @@ -2778,7 +2778,7 @@ protected T doFindAndReplace(String collectionName, Document mappedQuery, Do private T doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, - EntityProjectionIntrospector.EntityProjection projection) { + EntityProjection projection) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format( @@ -3241,10 +3241,10 @@ public T doWith(Document document) { private class ProjectingReadCallback implements DocumentCallback { private final MongoConverter reader; - private final EntityProjectionIntrospector.EntityProjection projection; + private final EntityProjection projection; private final String collectionName; - ProjectingReadCallback(MongoConverter reader, EntityProjectionIntrospector.EntityProjection projection, + ProjectingReadCallback(MongoConverter reader, EntityProjection projection, String collectionName) { this.reader = reader; 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 c13bed4833..8b3e1c2bc7 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 @@ -17,7 +17,7 @@ import org.bson.Document; -import org.springframework.data.mapping.context.EntityProjectionIntrospector; +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; @@ -48,7 +48,7 @@ class PropertyOperations { * @param fields must not be {@literal null}. * @return {@link Document} with fields to be included. */ - Document computeMappedFieldsForProjection(EntityProjectionIntrospector.EntityProjection projection, + Document computeMappedFieldsForProjection(EntityProjection projection, Document fields) { if (!projection.isProjection() || !projection.isClosedProjection()) { 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 70eadc0ad4..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 @@ -31,7 +31,7 @@ import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; -import org.springframework.data.mapping.context.EntityProjectionIntrospector; +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; @@ -289,7 +289,7 @@ Document getMappedQuery(@Nullable MongoPersistentEntity entity) { } Document getMappedFields(@Nullable MongoPersistentEntity entity, - EntityProjectionIntrospector.EntityProjection projection) { + EntityProjection projection) { Document fields = evaluateFields(entity); @@ -402,7 +402,7 @@ private DistinctQueryContext(@Nullable Object query, String fieldName) { @Override Document getMappedFields(@Nullable MongoPersistentEntity entity, - EntityProjectionIntrospector.EntityProjection projection) { + 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 590d700d66..87ebb9d99d 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,7 +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.EntityProjectionIntrospector; +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; @@ -1052,7 +1052,7 @@ protected Flux> geoNear(NearQuery near, Class entityClass, S String collection = StringUtils.hasText(collectionName) ? collectionName : getCollectionName(entityClass); String distanceField = operations.nearQueryDistanceFieldName(entityClass); - EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(returnType, + EntityProjection projection = operations.introspectProjection(returnType, entityClass); GeoNearResultDocumentCallback callback = new GeoNearResultDocumentCallback<>(distanceField, @@ -1135,7 +1135,7 @@ public Mono findAndReplace(Query query, S replacement, FindAndReplaceO MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityType); QueryContext queryContext = queryOperations.createQueryContext(query); - EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(resultType, + EntityProjection projection = operations.introspectProjection(resultType, entityType); Document mappedQuery = queryContext.getMappedQuery(entity); @@ -2373,7 +2373,7 @@ 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, - EntityProjectionIntrospector.EntityProjection.nonProjecting(entityClass)); + EntityProjection.nonProjecting(entityClass)); Document mappedQuery = queryContext.getMappedQuery(entity); if (LOGGER.isDebugEnabled()) { @@ -2426,7 +2426,7 @@ protected Flux doFind(String collectionName, Document query, Document QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); Document mappedFields = queryContext.getMappedFields(entity, - EntityProjectionIntrospector.EntityProjection.nonProjecting(entityClass)); + EntityProjection.nonProjecting(entityClass)); Document mappedQuery = queryContext.getMappedQuery(entity); if (LOGGER.isDebugEnabled()) { @@ -2448,7 +2448,7 @@ Flux doFind(String collectionName, Document query, Document fields, Cl Class targetClass, FindPublisherPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); - EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(targetClass, + EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); @@ -2596,7 +2596,7 @@ protected Mono doFindAndReplace(String collectionName, Document mappedQue Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { - EntityProjectionIntrospector.EntityProjection projection = operations.introspectProjection(resultType, + EntityProjection projection = operations.introspectProjection(resultType, entityType); return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement, @@ -2622,7 +2622,7 @@ protected Mono doFindAndReplace(String collectionName, Document mappedQue private Mono doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, - EntityProjectionIntrospector.EntityProjection projection) { + EntityProjection projection) { return Mono.defer(() -> { @@ -3218,10 +3218,10 @@ public Mono doWith(Document document) { private class ProjectingReadCallback implements DocumentCallback { private final MongoConverter reader; - private final EntityProjectionIntrospector.EntityProjection projection; + private final EntityProjection projection; private final String collectionName; - ProjectingReadCallback(MongoConverter reader, EntityProjectionIntrospector.EntityProjection projection, + ProjectingReadCallback(MongoConverter reader, EntityProjection projection, String collectionName) { this.reader = reader; this.projection = projection; 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 5479d717c2..250297442f 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 @@ -59,7 +59,7 @@ 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.EntityProjectionIntrospector; +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; @@ -300,7 +300,7 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) { } @Override - public R project(EntityProjectionIntrospector.EntityProjection projection, Bson bson) { + public R project(EntityProjection projection, Bson bson) { if (!projection.isProjection()) { // backed by real object return read(projection.getMappedType(), bson); @@ -315,7 +315,7 @@ public R project(EntityProjectionIntrospector.EntityProjection project @SuppressWarnings("unchecked") private R doReadProjection(ConversionContext context, Bson bson, - EntityProjectionIntrospector.EntityProjection projection) { + EntityProjection projection) { MongoPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(projection.getActualDomainType()); TypeInformation mappedType = projection.getActualMappedType(); @@ -377,7 +377,7 @@ String getFieldName(MongoPersistentProperty prop) { } private Object doReadOrProject(ConversionContext context, Bson source, TypeInformation typeHint, - EntityProjectionIntrospector.EntityProjection typeDescriptor) { + EntityProjection typeDescriptor) { if (typeDescriptor.isProjection()) { return doReadProjection(context, BsonUtils.asDocument(source), typeDescriptor); @@ -388,12 +388,12 @@ private Object doReadOrProject(ConversionContext context, Bson source, TypeInfor class ProjectingConversionContext extends ConversionContext { - private final EntityProjectionIntrospector.EntityProjection returnedTypeDescriptor; + private final EntityProjection returnedTypeDescriptor; ProjectingConversionContext(CustomConversions customConversions, ObjectPath path, ContainerValueConverter> collectionConverter, ContainerValueConverter mapConverter, ContainerValueConverter dbRefConverter, ValueConverter elementConverter, - EntityProjectionIntrospector.EntityProjection projection) { + EntityProjection projection) { super(customConversions, path, (context, source, typeHint) -> doReadOrProject(context, source, typeHint, projection), @@ -404,7 +404,7 @@ class ProjectingConversionContext extends ConversionContext { @Override public ConversionContext forProperty(String name) { - EntityProjectionIntrospector.EntityProjection property = returnedTypeDescriptor.findProperty(name); + EntityProjection property = returnedTypeDescriptor.findProperty(name); if (property == null) { return super.forProperty(name); } 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 b6cd10a687..651d4d07b5 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,12 +19,13 @@ 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.EntityProjectionIntrospector; +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; @@ -75,8 +76,8 @@ public interface MongoConverter /** * Apply a projection to {@link Bson} and return the projection return type {@code R}. - * {@link EntityProjectionIntrospector.EntityProjection#isProjection() Non-projecting} descriptors fall back to - * {@link #read(Class, Object) regular object materialization}. + * {@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}. @@ -84,7 +85,7 @@ public interface MongoConverter * @return a new instance of the projection return type {@code R}. * @since 2.7 */ - R project(EntityProjectionIntrospector.EntityProjection descriptor, Bson bson); + R project(EntityProjection descriptor, Bson bson); /** * Mapping function capable of converting values into a desired target type by eg. extracting the actual java type 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 2e4a1ca96d..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,11 +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.EntityProjectionIntrospector; +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; @@ -107,7 +108,7 @@ public CustomConversions getCustomConversions() { } @Override - public R project(EntityProjectionIntrospector.EntityProjection descriptor, Bson bson) { + public R project(EntityProjection descriptor, Bson bson) { return null; } }; 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 c3c32aafe0..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,10 +19,11 @@ 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.EntityProjectionIntrospector; +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; @@ -73,7 +74,7 @@ public CustomConversions getCustomConversions() { } @Override - public R project(EntityProjectionIntrospector.EntityProjection descriptor, Bson bson) { + public R project(EntityProjection descriptor, Bson bson) { return null; } 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 c543ad790e..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 @@ -67,6 +67,7 @@ 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; @@ -2650,7 +2651,7 @@ void projectShouldReadSimpleInterfaceProjection() { .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), mappingContext); - EntityProjectionIntrospector.EntityProjection projection = discoverer + EntityProjection projection = discoverer .introspect(PersonProjection.class, Person.class); PersonProjection person = converter.project(projection, source); @@ -2669,7 +2670,7 @@ void projectShouldReadSimpleDtoProjection() { .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), mappingContext); - EntityProjectionIntrospector.EntityProjection projection = introspector + EntityProjection projection = introspector .introspect(PersonDto.class, Person.class); PersonDto person = converter.project(projection, source); @@ -2688,7 +2689,7 @@ void projectShouldReadNestedProjection() { .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), mappingContext); - EntityProjectionIntrospector.EntityProjection projection = introspector + EntityProjection projection = introspector .introspect(WithNestedProjection.class, Person.class); WithNestedProjection person = converter.project(projection, source); @@ -2706,7 +2707,7 @@ void projectShouldReadProjectionWithNestedEntity() { .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), mappingContext); - EntityProjectionIntrospector.EntityProjection projection = introspector + EntityProjection projection = introspector .introspect(ProjectionWithNestedEntity.class, Person.class); ProjectionWithNestedEntity person = converter.project(projection, source); From 464076754df05821278f3d652efd295aa1a2dba4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 1 Dec 2021 14:41:58 +0100 Subject: [PATCH 6/8] Polishing. --- .../data/mongodb/core/MongoTemplate.java | 2 +- .../data/mongodb/core/PropertyOperations.java | 1 - .../data/mongodb/core/ReactiveMongoTemplate.java | 2 +- .../mongodb/core/convert/MappingMongoConverter.java | 13 +++++++++---- .../data/mongodb/core/convert/MongoConverter.java | 6 +++--- .../core/mapping/PersistentPropertyTranslator.java | 2 +- 6 files changed, 15 insertions(+), 11 deletions(-) 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 3feb4ba06b..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 @@ -2772,7 +2772,7 @@ protected T doFindAndReplace(String collectionName, Document mappedQuery, Do * @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 2.7 + * @since 3.4 */ @Nullable private T doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, 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 8b3e1c2bc7..753bcaa410 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 @@ -58,7 +58,6 @@ Document computeMappedFieldsForProjection(EntityProjection projection, Document projectedFields = new Document(); if (projection.getMappedType().getType().isInterface()) { - projection.forEach(propertyPath -> projectedFields.put(propertyPath.getSegment(), 1)); } else { 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 87ebb9d99d..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 @@ -2617,7 +2617,7 @@ protected Mono doFindAndReplace(String collectionName, Document mappedQue * @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 2.7 + * @since 3.4 */ private Mono doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, 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 250297442f..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 @@ -139,7 +139,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App private SpELContext spELContext; private @Nullable EntityCallbacks entityCallbacks; private final DocumentPointerFactory documentPointerFactory; - private final ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); /** * Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}. @@ -227,7 +227,7 @@ public MongoTypeMapper getTypeMapper() { @Override public ProjectionFactory getProjectionFactory() { - return factory; + return projectionFactory; } @Override @@ -277,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)); @@ -303,7 +305,10 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) { public R project(EntityProjection projection, Bson bson) { if (!projection.isProjection()) { // backed by real object - return read(projection.getMappedType(), bson); + + TypeInformation typeToRead = projection.getMappedType().getType().isInterface() ? projection.getDomainType() + : projection.getMappedType(); + return (R) read(typeToRead, bson); } ProjectingConversionContext context = new ProjectingConversionContext(conversions, ObjectPath.ROOT, @@ -337,7 +342,7 @@ private R doReadProjection(ConversionContext context, Bson bson, readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator, Predicates.isTrue()); - return (R) factory.createProjection(mappedType.getType(), accessor.getBean()); + return (R) projectionFactory.createProjection(mappedType.getType(), accessor.getBean()); } // DTO projection 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 651d4d07b5..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 @@ -62,7 +62,7 @@ public interface MongoConverter * Returns the {@link ProjectionFactory} for this converter. * * @return will never be {@literal null}. - * @since 2.7 + * @since 3.4 */ ProjectionFactory getProjectionFactory(); @@ -70,7 +70,7 @@ public interface MongoConverter * Returns the {@link CustomConversions} for this converter. * * @return will never be {@literal null}. - * @since 2.7 + * @since 3.4 */ CustomConversions getCustomConversions(); @@ -83,7 +83,7 @@ public interface MongoConverter * @param bson must not be {@literal null}. * @param * @return a new instance of the projection return type {@code R}. - * @since 2.7 + * @since 3.4 */ R project(EntityProjection descriptor, Bson bson); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/PersistentPropertyTranslator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/PersistentPropertyTranslator.java index 6ab7a4e4d7..4ef147d71e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/PersistentPropertyTranslator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/PersistentPropertyTranslator.java @@ -27,7 +27,7 @@ * Mainly used within the framework. * * @author Mark Paluch - * @since 2.7 + * @since 3.4 */ public class PersistentPropertyTranslator { From b40777584a32adaf2a39e672bde66c5c3f2d5552 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 8 Dec 2021 09:36:27 +0100 Subject: [PATCH 7/8] Adapt to changed signatures. --- .../springframework/data/mongodb/core/PropertyOperations.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 753bcaa410..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 @@ -58,7 +58,9 @@ Document computeMappedFieldsForProjection(EntityProjection projection, Document projectedFields = new Document(); if (projection.getMappedType().getType().isInterface()) { - projection.forEach(propertyPath -> projectedFields.put(propertyPath.getSegment(), 1)); + projection.forEach(it -> { + projectedFields.put(it.getPropertyPath().getSegment(), 1); + }); } else { // DTO projections use merged metadata between domain type and result type From 0016f70dea164506fc9cea32bdc270c9a14a6f96 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 10 Dec 2021 09:11:47 +0100 Subject: [PATCH 8/8] Add tests projecting references. --- .../ExecutableFindOperationSupportTests.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) 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 540334a4d3..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 @@ -44,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; @@ -549,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 @@ -618,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();