Browse Source

Polishing.

Extract Wrapped support to PersistentEntity for a central resolution of proxies before accessing their properties.

See #5031
Original pull request: #5033
pull/5039/head
Mark Paluch 4 months ago
parent
commit
2b4460701a
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 27
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java
  2. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java
  3. 12
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
  4. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java
  5. 43
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Wrapped.java
  6. 9
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java
  7. 108
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java
  8. 6
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java
  9. 11
      spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/auditing.xml

27
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.bson.Document;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.Wrapped;
import com.mongodb.DBRef; import com.mongodb.DBRef;
@ -32,23 +34,9 @@ import com.mongodb.DBRef;
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
*/ */
class DefaultDbRefProxyHandler implements DbRefProxyHandler { record DefaultDbRefProxyHandler(
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, ValueResolver resolver,
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext; Function<Object, ValueExpressionEvaluator> evaluatorFactory) implements DbRefProxyHandler {
private final ValueResolver resolver;
private final Function<Object, ValueExpressionEvaluator> evaluatorFactory;
/**
* @param mappingContext must not be {@literal null}.
* @param resolver must not be {@literal null}.
*/
public DefaultDbRefProxyHandler(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
ValueResolver resolver, Function<Object, ValueExpressionEvaluator> evaluatorFactory) {
this.mappingContext = mappingContext;
this.resolver = resolver;
this.evaluatorFactory = evaluatorFactory;
}
@Override @Override
public Object populateId(MongoPersistentProperty property, @Nullable DBRef source, Object proxy) { public Object populateId(MongoPersistentProperty property, @Nullable DBRef source, Object proxy) {
@ -65,11 +53,12 @@ class DefaultDbRefProxyHandler implements DbRefProxyHandler {
} }
ValueExpressionEvaluator evaluator = evaluatorFactory.apply(proxy); ValueExpressionEvaluator evaluator = evaluatorFactory.apply(proxy);
PersistentPropertyAccessor accessor = entity.getPropertyAccessor(proxy); PersistentPropertyAccessor<?> accessor = entity.getPropertyAccessor((Wrapped) () -> proxy);
Document object = new Document(idProperty.getFieldName(), source.getId()); Document object = new Document(idProperty.getFieldName(), source.getId());
ObjectPath objectPath = ObjectPath.ROOT.push(proxy, entity, null); 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; return proxy;
} }

5
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.jspecify.annotations.Nullable;
import org.springframework.data.mongodb.core.mapping.Wrapped;
import com.mongodb.DBRef; import com.mongodb.DBRef;
/** /**
@ -28,7 +30,7 @@ import com.mongodb.DBRef;
* @since 1.5 * @since 1.5
* @see LazyLoadingProxyFactory * @see LazyLoadingProxyFactory
*/ */
public interface LazyLoadingProxy { public interface LazyLoadingProxy extends Wrapped {
/** /**
* Initializes the proxy and returns the wrapped value. * Initializes the proxy and returns the wrapped value.
@ -36,6 +38,7 @@ public interface LazyLoadingProxy {
* @return * @return
* @since 1.5 * @since 1.5
*/ */
@Override
Object getTarget(); Object getTarget();
/** /**

12
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 java.util.Map;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression; 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.Association;
import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mongodb.MongoCollectionUtils; import org.springframework.data.mongodb.MongoCollectionUtils;
@ -188,6 +190,16 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
verifyFieldTypes(); verifyFieldTypes();
} }
@Override
public boolean isNew(Object bean) {
return super.isNew(Wrapped.getTargetObject(bean));
}
@Override
public <B> PersistentPropertyAccessor<B> getPropertyAccessor(B bean) {
return super.getPropertyAccessor(Wrapped.getTargetObject(bean));
}
@Override @Override
public EvaluationContext getEvaluationContext(@Nullable Object rootObject) { public EvaluationContext getEvaluationContext(@Nullable Object rootObject) {
return super.getEvaluationContext(rootObject); return super.getEvaluationContext(rootObject);

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

@ -83,7 +83,7 @@ public interface MongoPersistentEntity<T> extends MutablePersistentEntity<T, Mon
/** /**
* Get the entities shard key if defined. * Get the entities shard key if defined.
* *
* @return {@link ShardKey#none()} if not not set. * @return {@link ShardKey#none()} if not set.
* @since 3.0 * @since 3.0
*/ */
ShardKey getShardKey(); ShardKey getShardKey();

43
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Wrapped.java

@ -0,0 +1,43 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.mapping;
/**
* Marker interface for objects that wrap another object, providing a way to retrieve the wrapped value.
*
* @author Mark Paluch
* @since 5.0
*/
public interface Wrapped {
/**
* Returns the wrapped value.
*
* @return
*/
Object getTarget();
/**
* Unwraps the given object if it is an instance of {@link Wrapped}. If not, returns the object as-is.
*
* @param object the object to unwrap, must not be {@literal null}.
* @return the unwrapped object or the original object if it is not wrapped.
*/
static <T> T getTargetObject(T object) {
return object instanceof Wrapped w ? (T) w.getTarget() : object;
}
}

9
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.bson.types.ObjectId;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.data.mapping.PersistentPropertyAccessor; 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.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
@ -108,13 +108,6 @@ public class MappingMongoEntityInformation<T, ID> extends PersistentEntityInform
return fallbackIdType; return fallbackIdType;
} }
@Override
public boolean isNew(T entity) {
T unwrapped = entity instanceof LazyLoadingProxy proxy ? (T) proxy.getTarget() : entity;
return super.isNew(unwrapped);
}
@Override @Override
public boolean isVersioned() { public boolean isVersioned() {
return this.entityMetadata.hasVersionProperty(); return this.entityMetadata.hasVersionProperty();

108
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 java.util.Date;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.support.AbstractApplicationContext; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.mapping.callback.EntityCallbacks; 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.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; 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. * Integration test for the auditing support.
@ -36,14 +43,17 @@ import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback
* @author Oliver Gierke * @author Oliver Gierke
* @author Mark Paluch * @author Mark Paluch
*/ */
public class AuditingIntegrationTests { @ContextConfiguration(locations = "auditing.xml")
@ExtendWith(SpringExtension.class)
class AuditingIntegrationTests {
@Test // DATAMONGO-577, DATAMONGO-800, DATAMONGO-883, DATAMONGO-2261 @Autowired MongoMappingContext mappingContext;
public void enablesAuditingAndSetsPropertiesAccordingly() throws Exception { @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); mappingContext.getPersistentEntity(Entity.class);
EntityCallbacks callbacks = EntityCallbacks.create(context); EntityCallbacks callbacks = EntityCallbacks.create(context);
@ -55,18 +65,67 @@ public class AuditingIntegrationTests {
assertThat(entity.modified).isEqualTo(entity.created); assertThat(entity.modified).isEqualTo(entity.created);
Thread.sleep(10); Thread.sleep(10);
entity.id = 1L; entity.id = "foo";
entity = callbacks.callback(BeforeConvertCallback.class, entity, "collection-1"); entity = callbacks.callback(BeforeConvertCallback.class, entity, "collection-1");
assertThat(entity.created).isNotNull(); assertThat(entity.created).isNotNull();
assertThat(entity.modified).isNotEqualTo(entity.created); 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; @CreatedDate Date created;
Date modified; Date modified;
@ -74,5 +133,32 @@ public class AuditingIntegrationTests {
public Date getModified() { public Date getModified() {
return modified; 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;
}
} }

6
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.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.data.annotation.AccessType; import org.springframework.data.annotation.AccessType;
import org.springframework.data.annotation.AccessType.Type; import org.springframework.data.annotation.AccessType.Type;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.PersistenceCreator;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.MongoExceptionTranslator; import org.springframework.data.mongodb.core.MongoExceptionTranslator;
@ -487,11 +487,11 @@ class DbRefMappingMongoConverterUnitTests {
ClassWithLazyDbRefs result = converter.read(ClassWithLazyDbRefs.class, object); 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) MongoPersistentProperty idProperty = mappingContext.getRequiredPersistentEntity(LazyDbRefTarget.class)
.getIdProperty(); .getIdProperty();
assertThat(accessor.getProperty(idProperty)).isNotNull(); assertThat(accessor.getPropertyValue(idProperty.getName())).isNotNull();
assertProxyIsResolved(result.dbRefToConcreteType, false); assertProxyIsResolved(result.dbRefToConcreteType, false);
} }

11
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 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"> http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<mongo:auditing mapping-context-ref="customMappingContext" /> <mongo:auditing mapping-context-ref="mongoMappingContext"/>
<bean id="customMappingContext" class="org.springframework.data.mongodb.core.mapping.MongoMappingContext" /> <mongo:db-factory dbname="repositories"/>
<mongo:mapping-converter auto-index-creation="true"/>
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg ref="mongoDbFactory"/>
<constructor-arg ref="mappingConverter"/>
</bean>
</beans> </beans>

Loading…
Cancel
Save