From 2b4460701a37c86b69265efced0405199b307af3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 6 Aug 2025 17:26:54 +0200 Subject: [PATCH] Polishing. Extract Wrapped support to PersistentEntity for a central resolution of proxies before accessing their properties. See #5031 Original pull request: #5033 --- .../convert/DefaultDbRefProxyHandler.java | 27 ++--- .../core/convert/LazyLoadingProxy.java | 5 +- .../mapping/BasicMongoPersistentEntity.java | 12 ++ .../core/mapping/MongoPersistentEntity.java | 2 +- .../data/mongodb/core/mapping/Wrapped.java | 43 +++++++ .../MappingMongoEntityInformation.java | 9 +- .../config/AuditingIntegrationTests.java | 108 ++++++++++++++++-- .../DbRefMappingMongoConverterUnitTests.java | 6 +- .../data/mongodb/config/auditing.xml | 13 ++- 9 files changed, 179 insertions(+), 46 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Wrapped.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java index 13c0198aa..dc6f6a576 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java @@ -19,11 +19,13 @@ import java.util.function.Function; import org.bson.Document; import org.jspecify.annotations.Nullable; + import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.mapping.Wrapped; import com.mongodb.DBRef; @@ -32,23 +34,9 @@ import com.mongodb.DBRef; * @author Christoph Strobl * @author Mark Paluch */ -class DefaultDbRefProxyHandler implements DbRefProxyHandler { - - private final MappingContext, MongoPersistentProperty> mappingContext; - private final ValueResolver resolver; - private final Function evaluatorFactory; - - /** - * @param mappingContext must not be {@literal null}. - * @param resolver must not be {@literal null}. - */ - public DefaultDbRefProxyHandler(MappingContext, MongoPersistentProperty> mappingContext, - ValueResolver resolver, Function evaluatorFactory) { - - this.mappingContext = mappingContext; - this.resolver = resolver; - this.evaluatorFactory = evaluatorFactory; - } +record DefaultDbRefProxyHandler( + MappingContext, MongoPersistentProperty> mappingContext, ValueResolver resolver, + Function evaluatorFactory) implements DbRefProxyHandler { @Override public Object populateId(MongoPersistentProperty property, @Nullable DBRef source, Object proxy) { @@ -65,11 +53,12 @@ class DefaultDbRefProxyHandler implements DbRefProxyHandler { } ValueExpressionEvaluator evaluator = evaluatorFactory.apply(proxy); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(proxy); + PersistentPropertyAccessor accessor = entity.getPropertyAccessor((Wrapped) () -> proxy); Document object = new Document(idProperty.getFieldName(), source.getId()); ObjectPath objectPath = ObjectPath.ROOT.push(proxy, entity, null); - accessor.setProperty(idProperty, resolver.getValueInternal(idProperty, object, evaluator, objectPath)); + accessor.setProperty(idProperty, + resolver.getValueInternal(idProperty, object, evaluator, objectPath)); return proxy; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java index 6329d74d4..241ba4647 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java @@ -17,6 +17,8 @@ package org.springframework.data.mongodb.core.convert; import org.jspecify.annotations.Nullable; +import org.springframework.data.mongodb.core.mapping.Wrapped; + import com.mongodb.DBRef; /** @@ -28,7 +30,7 @@ import com.mongodb.DBRef; * @since 1.5 * @see LazyLoadingProxyFactory */ -public interface LazyLoadingProxy { +public interface LazyLoadingProxy extends Wrapped { /** * Initializes the proxy and returns the wrapped value. @@ -36,6 +38,7 @@ public interface LazyLoadingProxy { * @return * @since 1.5 */ + @Override Object getTarget(); /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java index e86500931..f302c3657 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import org.jspecify.annotations.Nullable; + import org.springframework.data.annotation.Id; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueExpression; @@ -33,6 +34,7 @@ import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.mongodb.MongoCollectionUtils; @@ -188,6 +190,16 @@ public class BasicMongoPersistentEntity extends BasicPersistentEntity PersistentPropertyAccessor getPropertyAccessor(B bean) { + return super.getPropertyAccessor(Wrapped.getTargetObject(bean)); + } + @Override public EvaluationContext getEvaluationContext(@Nullable Object rootObject) { return super.getEvaluationContext(rootObject); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java index f1d67e4ae..7eb07a109 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java @@ -83,7 +83,7 @@ public interface MongoPersistentEntity extends MutablePersistentEntity T getTargetObject(T object) { + return object instanceof Wrapped w ? (T) w.getTarget() : object; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java index 647f8531a..c3989b7f7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java @@ -17,8 +17,8 @@ package org.springframework.data.mongodb.repository.support; import org.bson.types.ObjectId; import org.jspecify.annotations.Nullable; + import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mongodb.core.convert.LazyLoadingProxy; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; @@ -108,13 +108,6 @@ public class MappingMongoEntityInformation extends PersistentEntityInform return fallbackIdType; } - @Override - public boolean isNew(T entity) { - - T unwrapped = entity instanceof LazyLoadingProxy proxy ? (T) proxy.getTarget() : entity; - return super.isNew(unwrapped); - } - @Override public boolean isVersioned() { return this.entityMetadata.hasVersionProperty(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java index ee411eb7c..543e06b02 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java @@ -19,16 +19,23 @@ import static org.assertj.core.api.Assertions.*; import java.util.Date; +import org.bson.types.ObjectId; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.context.support.AbstractApplicationContext; -import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; /** * Integration test for the auditing support. @@ -36,14 +43,17 @@ import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback * @author Oliver Gierke * @author Mark Paluch */ -public class AuditingIntegrationTests { +@ContextConfiguration(locations = "auditing.xml") +@ExtendWith(SpringExtension.class) +class AuditingIntegrationTests { - @Test // DATAMONGO-577, DATAMONGO-800, DATAMONGO-883, DATAMONGO-2261 - public void enablesAuditingAndSetsPropertiesAccordingly() throws Exception { + @Autowired MongoMappingContext mappingContext; + @Autowired ApplicationContext context; + @Autowired MongoTemplate template; - AbstractApplicationContext context = new ClassPathXmlApplicationContext("auditing.xml", getClass()); + @Test // DATAMONGO-577, DATAMONGO-800, DATAMONGO-883, DATAMONGO-2261 + void enablesAuditingAndSetsPropertiesAccordingly() throws Exception { - MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class); mappingContext.getPersistentEntity(Entity.class); EntityCallbacks callbacks = EntityCallbacks.create(context); @@ -55,18 +65,67 @@ public class AuditingIntegrationTests { assertThat(entity.modified).isEqualTo(entity.created); Thread.sleep(10); - entity.id = 1L; + entity.id = "foo"; entity = callbacks.callback(BeforeConvertCallback.class, entity, "collection-1"); assertThat(entity.created).isNotNull(); assertThat(entity.modified).isNotEqualTo(entity.created); - context.close(); } - class Entity { + @Test // GH-5031 + void handlesDocumentReferenceAuditingCorrectly() throws InterruptedException { + + template.remove(TopDocumentReferenceLevelEntity.class).all(); + + Entity entity = new Entity(); + template.insert(entity); + + TopDocumentReferenceLevelEntity tle = new TopDocumentReferenceLevelEntity(); + tle.entity = entity; + template.insert(tle); + + Thread.sleep(200); + + TopDocumentReferenceLevelEntity loadAndModify = template.findById(tle.id, TopDocumentReferenceLevelEntity.class); + Date created = loadAndModify.entity.getCreated(); + Date modified = loadAndModify.entity.getModified(); + template.save(loadAndModify.entity); + + TopDocumentReferenceLevelEntity loaded = template.findById(tle.id, TopDocumentReferenceLevelEntity.class); + + assertThat(loaded.entity.getCreated()).isEqualTo(created); + assertThat(loaded.entity.getModified()).isNotEqualTo(modified); + } + + @Test // GH-5031 + void handlesDbRefAuditingCorrectly() throws InterruptedException { + + template.remove(TopDbRefLevelEntity.class).all(); + + Entity entity = new Entity(); + template.insert(entity); + + TopDbRefLevelEntity tle = new TopDbRefLevelEntity(); + tle.entity = entity; + template.insert(tle); + + Thread.sleep(200); + + TopDbRefLevelEntity loadAndModify = template.findById(tle.id, TopDbRefLevelEntity.class); + Date created = loadAndModify.entity.getCreated(); + Date modified = loadAndModify.entity.getModified(); + template.save(loadAndModify.entity); + + TopDbRefLevelEntity loaded = template.findById(tle.id, TopDbRefLevelEntity.class); - @Id Long id; + assertThat(loaded.entity.getCreated()).isEqualTo(created); + assertThat(loaded.entity.getModified()).isNotEqualTo(modified); + } + + static class Entity { + + @Id String id; @CreatedDate Date created; Date modified; @@ -74,5 +133,32 @@ public class AuditingIntegrationTests { public Date getModified() { return modified; } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setModified(Date modified) { + this.modified = modified; + } } + + static class TopDocumentReferenceLevelEntity { + + @Id ObjectId id; + @DocumentReference(lazy = true) Entity entity; + + } + + static class TopDbRefLevelEntity { + + @Id ObjectId id; + @DBRef(lazy = true) Entity entity; + + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java index 2628a3a5c..c077ed646 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java @@ -44,11 +44,11 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.data.annotation.AccessType; import org.springframework.data.annotation.AccessType.Type; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceCreator; -import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.MongoExceptionTranslator; @@ -487,11 +487,11 @@ class DbRefMappingMongoConverterUnitTests { ClassWithLazyDbRefs result = converter.read(ClassWithLazyDbRefs.class, object); - PersistentPropertyAccessor accessor = propertyEntity.getPropertyAccessor(result.dbRefToConcreteType); + DirectFieldAccessor accessor = new DirectFieldAccessor(result.dbRefToConcreteType); MongoPersistentProperty idProperty = mappingContext.getRequiredPersistentEntity(LazyDbRefTarget.class) .getIdProperty(); - assertThat(accessor.getProperty(idProperty)).isNotNull(); + assertThat(accessor.getPropertyValue(idProperty.getName())).isNotNull(); assertProxyIsResolved(result.dbRefToConcreteType, false); } diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/auditing.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/auditing.xml index 8466692f8..234f3cd7b 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/auditing.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/auditing.xml @@ -5,8 +5,15 @@ xsi:schemaLocation="http://www.springframework.org/schema/data/mongo https://www.springframework.org/schema/data/mongo/spring-mongo.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - - - + + + + + + + + + +