Skip to content

Commit 2b44607

Browse files
committed
Polishing.
Extract Wrapped support to PersistentEntity for a central resolution of proxies before accessing their properties. See #5031 Original pull request: #5033
1 parent bcb40b1 commit 2b44607

File tree

9 files changed

+179
-46
lines changed

9 files changed

+179
-46
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919

2020
import org.bson.Document;
2121
import org.jspecify.annotations.Nullable;
22+
2223
import org.springframework.data.mapping.PersistentPropertyAccessor;
2324
import org.springframework.data.mapping.context.MappingContext;
2425
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
2526
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
2627
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
28+
import org.springframework.data.mongodb.core.mapping.Wrapped;
2729

2830
import com.mongodb.DBRef;
2931

@@ -32,23 +34,9 @@
3234
* @author Christoph Strobl
3335
* @author Mark Paluch
3436
*/
35-
class DefaultDbRefProxyHandler implements DbRefProxyHandler {
36-
37-
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
38-
private final ValueResolver resolver;
39-
private final Function<Object, ValueExpressionEvaluator> evaluatorFactory;
40-
41-
/**
42-
* @param mappingContext must not be {@literal null}.
43-
* @param resolver must not be {@literal null}.
44-
*/
45-
public DefaultDbRefProxyHandler(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
46-
ValueResolver resolver, Function<Object, ValueExpressionEvaluator> evaluatorFactory) {
47-
48-
this.mappingContext = mappingContext;
49-
this.resolver = resolver;
50-
this.evaluatorFactory = evaluatorFactory;
51-
}
37+
record DefaultDbRefProxyHandler(
38+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, ValueResolver resolver,
39+
Function<Object, ValueExpressionEvaluator> evaluatorFactory) implements DbRefProxyHandler {
5240

5341
@Override
5442
public Object populateId(MongoPersistentProperty property, @Nullable DBRef source, Object proxy) {
@@ -65,11 +53,12 @@ public Object populateId(MongoPersistentProperty property, @Nullable DBRef sourc
6553
}
6654

6755
ValueExpressionEvaluator evaluator = evaluatorFactory.apply(proxy);
68-
PersistentPropertyAccessor accessor = entity.getPropertyAccessor(proxy);
56+
PersistentPropertyAccessor<?> accessor = entity.getPropertyAccessor((Wrapped) () -> proxy);
6957

7058
Document object = new Document(idProperty.getFieldName(), source.getId());
7159
ObjectPath objectPath = ObjectPath.ROOT.push(proxy, entity, null);
72-
accessor.setProperty(idProperty, resolver.getValueInternal(idProperty, object, evaluator, objectPath));
60+
accessor.setProperty(idProperty,
61+
resolver.getValueInternal(idProperty, object, evaluator, objectPath));
7362

7463
return proxy;
7564
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import org.jspecify.annotations.Nullable;
1919

20+
import org.springframework.data.mongodb.core.mapping.Wrapped;
21+
2022
import com.mongodb.DBRef;
2123

2224
/**
@@ -28,14 +30,15 @@
2830
* @since 1.5
2931
* @see LazyLoadingProxyFactory
3032
*/
31-
public interface LazyLoadingProxy {
33+
public interface LazyLoadingProxy extends Wrapped {
3234

3335
/**
3436
* Initializes the proxy and returns the wrapped value.
3537
*
3638
* @return
3739
* @since 1.5
3840
*/
41+
@Override
3942
Object getTarget();
4043

4144
/**

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@
2626
import java.util.Map;
2727

2828
import org.jspecify.annotations.Nullable;
29+
2930
import org.springframework.data.annotation.Id;
3031
import org.springframework.data.expression.ValueEvaluationContext;
3132
import org.springframework.data.expression.ValueExpression;
3233
import org.springframework.data.expression.ValueExpressionParser;
3334
import org.springframework.data.mapping.Association;
3435
import org.springframework.data.mapping.AssociationHandler;
3536
import org.springframework.data.mapping.MappingException;
37+
import org.springframework.data.mapping.PersistentPropertyAccessor;
3638
import org.springframework.data.mapping.PropertyHandler;
3739
import org.springframework.data.mapping.model.BasicPersistentEntity;
3840
import org.springframework.data.mongodb.MongoCollectionUtils;
@@ -188,6 +190,16 @@ public void verify() {
188190
verifyFieldTypes();
189191
}
190192

193+
@Override
194+
public boolean isNew(Object bean) {
195+
return super.isNew(Wrapped.getTargetObject(bean));
196+
}
197+
198+
@Override
199+
public <B> PersistentPropertyAccessor<B> getPropertyAccessor(B bean) {
200+
return super.getPropertyAccessor(Wrapped.getTargetObject(bean));
201+
}
202+
191203
@Override
192204
public EvaluationContext getEvaluationContext(@Nullable Object rootObject) {
193205
return super.getEvaluationContext(rootObject);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ default boolean hasCollation() {
8383
/**
8484
* Get the entities shard key if defined.
8585
*
86-
* @return {@link ShardKey#none()} if not not set.
86+
* @return {@link ShardKey#none()} if not set.
8787
* @since 3.0
8888
*/
8989
ShardKey getShardKey();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.mapping;
17+
18+
/**
19+
* Marker interface for objects that wrap another object, providing a way to retrieve the wrapped value.
20+
*
21+
* @author Mark Paluch
22+
* @since 5.0
23+
*/
24+
public interface Wrapped {
25+
26+
/**
27+
* Returns the wrapped value.
28+
*
29+
* @return
30+
*/
31+
Object getTarget();
32+
33+
/**
34+
* Unwraps the given object if it is an instance of {@link Wrapped}. If not, returns the object as-is.
35+
*
36+
* @param object the object to unwrap, must not be {@literal null}.
37+
* @return the unwrapped object or the original object if it is not wrapped.
38+
*/
39+
static <T> T getTargetObject(T object) {
40+
return object instanceof Wrapped w ? (T) w.getTarget() : object;
41+
}
42+
43+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717

1818
import org.bson.types.ObjectId;
1919
import org.jspecify.annotations.Nullable;
20+
2021
import org.springframework.data.mapping.PersistentPropertyAccessor;
21-
import org.springframework.data.mongodb.core.convert.LazyLoadingProxy;
2222
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
2323
import org.springframework.data.mongodb.core.query.Collation;
2424
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
@@ -108,13 +108,6 @@ public Class<ID> getIdType() {
108108
return fallbackIdType;
109109
}
110110

111-
@Override
112-
public boolean isNew(T entity) {
113-
114-
T unwrapped = entity instanceof LazyLoadingProxy proxy ? (T) proxy.getTarget() : entity;
115-
return super.isNew(unwrapped);
116-
}
117-
118111
@Override
119112
public boolean isVersioned() {
120113
return this.entityMetadata.hasVersionProperty();

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,41 @@
1919

2020
import java.util.Date;
2121

22+
import org.bson.types.ObjectId;
2223
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.ExtendWith;
2325

24-
import org.springframework.context.support.AbstractApplicationContext;
25-
import org.springframework.context.support.ClassPathXmlApplicationContext;
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.context.ApplicationContext;
2628
import org.springframework.data.annotation.CreatedDate;
2729
import org.springframework.data.annotation.Id;
2830
import org.springframework.data.annotation.LastModifiedDate;
2931
import org.springframework.data.mapping.callback.EntityCallbacks;
32+
import org.springframework.data.mongodb.core.MongoTemplate;
33+
import org.springframework.data.mongodb.core.mapping.DBRef;
34+
import org.springframework.data.mongodb.core.mapping.DocumentReference;
3035
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
3136
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback;
37+
import org.springframework.test.context.ContextConfiguration;
38+
import org.springframework.test.context.junit.jupiter.SpringExtension;
3239

3340
/**
3441
* Integration test for the auditing support.
3542
*
3643
* @author Oliver Gierke
3744
* @author Mark Paluch
3845
*/
39-
public class AuditingIntegrationTests {
46+
@ContextConfiguration(locations = "auditing.xml")
47+
@ExtendWith(SpringExtension.class)
48+
class AuditingIntegrationTests {
4049

41-
@Test // DATAMONGO-577, DATAMONGO-800, DATAMONGO-883, DATAMONGO-2261
42-
public void enablesAuditingAndSetsPropertiesAccordingly() throws Exception {
50+
@Autowired MongoMappingContext mappingContext;
51+
@Autowired ApplicationContext context;
52+
@Autowired MongoTemplate template;
4353

44-
AbstractApplicationContext context = new ClassPathXmlApplicationContext("auditing.xml", getClass());
54+
@Test // DATAMONGO-577, DATAMONGO-800, DATAMONGO-883, DATAMONGO-2261
55+
void enablesAuditingAndSetsPropertiesAccordingly() throws Exception {
4556

46-
MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class);
4757
mappingContext.getPersistentEntity(Entity.class);
4858

4959
EntityCallbacks callbacks = EntityCallbacks.create(context);
@@ -55,24 +65,100 @@ public void enablesAuditingAndSetsPropertiesAccordingly() throws Exception {
5565
assertThat(entity.modified).isEqualTo(entity.created);
5666

5767
Thread.sleep(10);
58-
entity.id = 1L;
68+
entity.id = "foo";
5969

6070
entity = callbacks.callback(BeforeConvertCallback.class, entity, "collection-1");
6171

6272
assertThat(entity.created).isNotNull();
6373
assertThat(entity.modified).isNotEqualTo(entity.created);
64-
context.close();
6574
}
6675

67-
class Entity {
76+
@Test // GH-5031
77+
void handlesDocumentReferenceAuditingCorrectly() throws InterruptedException {
78+
79+
template.remove(TopDocumentReferenceLevelEntity.class).all();
80+
81+
Entity entity = new Entity();
82+
template.insert(entity);
83+
84+
TopDocumentReferenceLevelEntity tle = new TopDocumentReferenceLevelEntity();
85+
tle.entity = entity;
86+
template.insert(tle);
87+
88+
Thread.sleep(200);
89+
90+
TopDocumentReferenceLevelEntity loadAndModify = template.findById(tle.id, TopDocumentReferenceLevelEntity.class);
91+
Date created = loadAndModify.entity.getCreated();
92+
Date modified = loadAndModify.entity.getModified();
93+
template.save(loadAndModify.entity);
94+
95+
TopDocumentReferenceLevelEntity loaded = template.findById(tle.id, TopDocumentReferenceLevelEntity.class);
96+
97+
assertThat(loaded.entity.getCreated()).isEqualTo(created);
98+
assertThat(loaded.entity.getModified()).isNotEqualTo(modified);
99+
}
100+
101+
@Test // GH-5031
102+
void handlesDbRefAuditingCorrectly() throws InterruptedException {
103+
104+
template.remove(TopDbRefLevelEntity.class).all();
105+
106+
Entity entity = new Entity();
107+
template.insert(entity);
108+
109+
TopDbRefLevelEntity tle = new TopDbRefLevelEntity();
110+
tle.entity = entity;
111+
template.insert(tle);
112+
113+
Thread.sleep(200);
114+
115+
TopDbRefLevelEntity loadAndModify = template.findById(tle.id, TopDbRefLevelEntity.class);
116+
Date created = loadAndModify.entity.getCreated();
117+
Date modified = loadAndModify.entity.getModified();
118+
template.save(loadAndModify.entity);
119+
120+
TopDbRefLevelEntity loaded = template.findById(tle.id, TopDbRefLevelEntity.class);
68121

69-
@Id Long id;
122+
assertThat(loaded.entity.getCreated()).isEqualTo(created);
123+
assertThat(loaded.entity.getModified()).isNotEqualTo(modified);
124+
}
125+
126+
static class Entity {
127+
128+
@Id String id;
70129
@CreatedDate Date created;
71130
Date modified;
72131

73132
@LastModifiedDate
74133
public Date getModified() {
75134
return modified;
76135
}
136+
137+
public Date getCreated() {
138+
return created;
139+
}
140+
141+
public void setCreated(Date created) {
142+
this.created = created;
143+
}
144+
145+
public void setModified(Date modified) {
146+
this.modified = modified;
147+
}
77148
}
149+
150+
static class TopDocumentReferenceLevelEntity {
151+
152+
@Id ObjectId id;
153+
@DocumentReference(lazy = true) Entity entity;
154+
155+
}
156+
157+
static class TopDbRefLevelEntity {
158+
159+
@Id ObjectId id;
160+
@DBRef(lazy = true) Entity entity;
161+
162+
}
163+
78164
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@
4444
import org.mockito.Mockito;
4545
import org.mockito.junit.jupiter.MockitoExtension;
4646

47+
import org.springframework.beans.DirectFieldAccessor;
4748
import org.springframework.data.annotation.AccessType;
4849
import org.springframework.data.annotation.AccessType.Type;
4950
import org.springframework.data.annotation.Id;
5051
import org.springframework.data.annotation.PersistenceCreator;
51-
import org.springframework.data.mapping.PersistentPropertyAccessor;
5252
import org.springframework.data.mapping.PropertyPath;
5353
import org.springframework.data.mongodb.MongoDatabaseFactory;
5454
import org.springframework.data.mongodb.core.MongoExceptionTranslator;
@@ -487,11 +487,11 @@ void shouldEagerlyResolveIdPropertyWithFieldAccess() {
487487

488488
ClassWithLazyDbRefs result = converter.read(ClassWithLazyDbRefs.class, object);
489489

490-
PersistentPropertyAccessor accessor = propertyEntity.getPropertyAccessor(result.dbRefToConcreteType);
490+
DirectFieldAccessor accessor = new DirectFieldAccessor(result.dbRefToConcreteType);
491491
MongoPersistentProperty idProperty = mappingContext.getRequiredPersistentEntity(LazyDbRefTarget.class)
492492
.getIdProperty();
493493

494-
assertThat(accessor.getProperty(idProperty)).isNotNull();
494+
assertThat(accessor.getPropertyValue(idProperty.getName())).isNotNull();
495495
assertProxyIsResolved(result.dbRefToConcreteType, false);
496496
}
497497

spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/auditing.xml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@
55
xsi:schemaLocation="http://www.springframework.org/schema/data/mongo https://www.springframework.org/schema/data/mongo/spring-mongo.xsd
66
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
77

8-
<mongo:auditing mapping-context-ref="customMappingContext" />
9-
10-
<bean id="customMappingContext" class="org.springframework.data.mongodb.core.mapping.MongoMappingContext" />
8+
<mongo:auditing mapping-context-ref="mongoMappingContext"/>
9+
10+
<mongo:db-factory dbname="repositories"/>
11+
12+
<mongo:mapping-converter auto-index-creation="true"/>
13+
14+
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
15+
<constructor-arg ref="mongoDbFactory"/>
16+
<constructor-arg ref="mappingConverter"/>
17+
</bean>
1118

1219
</beans>

0 commit comments

Comments
 (0)