Skip to content

Commit 48aabfb

Browse files
christophstroblmp911de
authored andcommitted
Fix non-association mapping when id value matches already resolved instance of same type.
This commit ensures to fully resolve non association values from the given source document instead of trying attempt a by id lookup in already resolved instances. Closes: #4098 Original pull request: #4133.
1 parent e1e8684 commit 48aabfb

File tree

2 files changed

+132
-32
lines changed

2 files changed

+132
-32
lines changed

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java

+113-27
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import org.bson.conversions.Bson;
3939
import org.bson.json.JsonReader;
4040
import org.bson.types.ObjectId;
41-
4241
import org.springframework.beans.BeansException;
4342
import org.springframework.beans.factory.BeanClassLoaderAware;
4443
import org.springframework.context.ApplicationContext;
@@ -187,7 +186,7 @@ protected ConversionContext getConversionContext(ObjectPath path) {
187186

188187
Assert.notNull(path, "ObjectPath must not be null");
189188

190-
return new ConversionContext(this, conversions, path, this::readDocument, this::readCollectionOrArray,
189+
return new DefaultConversionContext(this, conversions, path, this::readDocument, this::readCollectionOrArray,
191190
this::readMap, this::readDBRef, this::getPotentiallyConvertedSimpleRead);
192191
}
193192

@@ -396,7 +395,46 @@ private Object doReadOrProject(ConversionContext context, Bson source, TypeInfor
396395
return readDocument(context, source, typeHint);
397396
}
398397

399-
class ProjectingConversionContext extends ConversionContext {
398+
static class AssociationConversionContext implements ConversionContext {
399+
400+
private final ConversionContext delegate;
401+
402+
public AssociationConversionContext(ConversionContext delegate) {
403+
this.delegate = delegate;
404+
}
405+
406+
@Override
407+
public <S> S convert(Object source, TypeInformation<? extends S> typeHint, ConversionContext context) {
408+
return delegate.convert(source, typeHint, context);
409+
}
410+
411+
@Override
412+
public ConversionContext withPath(ObjectPath currentPath) {
413+
return new AssociationConversionContext(delegate.withPath(currentPath));
414+
}
415+
416+
@Override
417+
public ObjectPath getPath() {
418+
return delegate.getPath();
419+
}
420+
421+
@Override
422+
public CustomConversions getCustomConversions() {
423+
return delegate.getCustomConversions();
424+
}
425+
426+
@Override
427+
public MongoConverter getSourceConverter() {
428+
return delegate.getSourceConverter();
429+
}
430+
431+
@Override
432+
public boolean resolveIdsInContext() {
433+
return true;
434+
}
435+
}
436+
437+
class ProjectingConversionContext extends DefaultConversionContext {
400438

401439
private final EntityProjection<?, ?> returnedTypeDescriptor;
402440

@@ -412,20 +450,21 @@ class ProjectingConversionContext extends ConversionContext {
412450
}
413451

414452
@Override
415-
public ConversionContext forProperty(String name) {
453+
public DefaultConversionContext forProperty(String name) {
416454

417455
EntityProjection<?, ?> property = returnedTypeDescriptor.findProperty(name);
418456
if (property == null) {
419-
return new ConversionContext(sourceConverter, conversions, path, MappingMongoConverter.this::readDocument,
420-
collectionConverter, mapConverter, dbRefConverter, elementConverter);
457+
return new DefaultConversionContext(sourceConverter, conversions, path,
458+
MappingMongoConverter.this::readDocument, collectionConverter, mapConverter, dbRefConverter,
459+
elementConverter);
421460
}
422461

423462
return new ProjectingConversionContext(sourceConverter, conversions, path, collectionConverter, mapConverter,
424463
dbRefConverter, elementConverter, property);
425464
}
426465

427466
@Override
428-
public ConversionContext withPath(ObjectPath currentPath) {
467+
public DefaultConversionContext withPath(ObjectPath currentPath) {
429468
return new ProjectingConversionContext(sourceConverter, conversions, currentPath, collectionConverter,
430469
mapConverter, dbRefConverter, elementConverter, returnedTypeDescriptor);
431470
}
@@ -544,7 +583,7 @@ private <S> S read(ConversionContext context, MongoPersistentEntity<S> entity, D
544583
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext);
545584
DocumentAccessor documentAccessor = new DocumentAccessor(bson);
546585

547-
if (hasIdentifier(bson)) {
586+
if (context.resolveIdsInContext() && hasIdentifier(bson)) {
548587
S existing = findContextualEntity(context, entity, bson);
549588
if (existing != null) {
550589
return existing;
@@ -647,7 +686,7 @@ private void readProperties(ConversionContext context, MongoPersistentEntity<?>
647686
continue;
648687
}
649688

650-
ConversionContext propertyContext = context.forProperty(prop.getName());
689+
ConversionContext propertyContext = context.forProperty(prop);
651690
MongoDbPropertyValueProvider valueProviderToUse = valueProvider.withContext(propertyContext);
652691

653692
if (prop.isAssociation() && !entity.isConstructorArgument(prop)) {
@@ -721,7 +760,7 @@ private void readAssociation(Association<MongoPersistentProperty> association, P
721760
accessor.setProperty(property,
722761
dbRefResolver.resolveReference(property,
723762
new DocumentReferenceSource(documentAccessor.getDocument(), documentAccessor.get(property)),
724-
referenceLookupDelegate, context::convert));
763+
referenceLookupDelegate, context.forProperty(property)::convert));
725764
}
726765
return;
727766
}
@@ -1971,13 +2010,13 @@ public <T> T getPropertyValue(MongoPersistentProperty property) {
19712010
return null;
19722011
}
19732012

1974-
CustomConversions conversions = context.conversions;
2013+
CustomConversions conversions = context.getCustomConversions();
19752014
if (conversions.getPropertyValueConversions().hasValueConverter(property)) {
19762015
return (T) conversions.getPropertyValueConversions().getValueConverter(property).read(value,
1977-
new MongoConversionContext(property, context.sourceConverter));
2016+
new MongoConversionContext(property, context.getSourceConverter()));
19782017
}
19792018

1980-
ConversionContext contextToUse = context.forProperty(property.getName());
2019+
ConversionContext contextToUse = context.forProperty(property);
19812020

19822021
return (T) contextToUse.convert(value, property.getTypeInformation());
19832022
}
@@ -2201,13 +2240,49 @@ public TypeDescriptor toTypeDescriptor() {
22012240
}
22022241
}
22032242

2243+
interface ConversionContext {
2244+
2245+
default <S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint) {
2246+
return convert(source, typeHint, this);
2247+
}
2248+
2249+
<S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint, ConversionContext context);
2250+
2251+
ConversionContext withPath(ObjectPath currentPath);
2252+
2253+
ObjectPath getPath();
2254+
2255+
default ConversionContext forProperty(String name) {
2256+
return this;
2257+
}
2258+
2259+
default ConversionContext forProperty(@Nullable PersistentProperty property) {
2260+
2261+
if (property != null) {
2262+
if (property.isAssociation()) {
2263+
return new AssociationConversionContext(forProperty(property.getName()));
2264+
}
2265+
return forProperty(property.getName());
2266+
}
2267+
return this;
2268+
}
2269+
2270+
default boolean resolveIdsInContext() {
2271+
return false;
2272+
}
2273+
2274+
CustomConversions getCustomConversions();
2275+
2276+
MongoConverter getSourceConverter();
2277+
}
2278+
22042279
/**
22052280
* Conversion context holding references to simple {@link ValueConverter} and {@link ContainerValueConverter}.
22062281
* Entrypoint for recursive conversion of {@link Document} and other types.
22072282
*
22082283
* @since 3.2
22092284
*/
2210-
protected static class ConversionContext {
2285+
protected static class DefaultConversionContext implements ConversionContext {
22112286

22122287
final MongoConverter sourceConverter;
22132288
final org.springframework.data.convert.CustomConversions conversions;
@@ -2218,7 +2293,7 @@ protected static class ConversionContext {
22182293
final ContainerValueConverter<DBRef> dbRefConverter;
22192294
final ValueConverter<Object> elementConverter;
22202295

2221-
ConversionContext(MongoConverter sourceConverter,
2296+
DefaultConversionContext(MongoConverter sourceConverter,
22222297
org.springframework.data.convert.CustomConversions customConversions, ObjectPath path,
22232298
ContainerValueConverter<Bson> documentConverter, ContainerValueConverter<Collection<?>> collectionConverter,
22242299
ContainerValueConverter<Bson> mapConverter, ContainerValueConverter<DBRef> dbRefConverter,
@@ -2242,7 +2317,8 @@ protected static class ConversionContext {
22422317
* @return the converted object.
22432318
*/
22442319
@SuppressWarnings("unchecked")
2245-
public <S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint) {
2320+
public <S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint,
2321+
ConversionContext context) {
22462322

22472323
Assert.notNull(source, "Source must not be null");
22482324
Assert.notNull(typeHint, "TypeInformation must not be null");
@@ -2262,26 +2338,26 @@ public <S extends Object> S convert(Object source, TypeInformation<? extends S>
22622338
}
22632339

22642340
if (typeHint.isCollectionLike() || typeHint.getType().isAssignableFrom(Collection.class)) {
2265-
return (S) collectionConverter.convert(this, (Collection<?>) source, typeHint);
2341+
return (S) collectionConverter.convert(context, (Collection<?>) source, typeHint);
22662342
}
22672343
}
22682344

22692345
if (typeHint.isMap()) {
22702346

22712347
if (ClassUtils.isAssignable(Document.class, typeHint.getType())) {
2272-
return (S) documentConverter.convert(this, BsonUtils.asBson(source), typeHint);
2348+
return (S) documentConverter.convert(context, BsonUtils.asBson(source), typeHint);
22732349
}
22742350

22752351
if (BsonUtils.supportsBson(source)) {
2276-
return (S) mapConverter.convert(this, BsonUtils.asBson(source), typeHint);
2352+
return (S) mapConverter.convert(context, BsonUtils.asBson(source), typeHint);
22772353
}
22782354

22792355
throw new IllegalArgumentException(
22802356
String.format("Expected map like structure but found %s", source.getClass()));
22812357
}
22822358

22832359
if (source instanceof DBRef) {
2284-
return (S) dbRefConverter.convert(this, (DBRef) source, typeHint);
2360+
return (S) dbRefConverter.convert(context, (DBRef) source, typeHint);
22852361
}
22862362

22872363
if (source instanceof Collection) {
@@ -2290,31 +2366,41 @@ public <S extends Object> S convert(Object source, TypeInformation<? extends S>
22902366
}
22912367

22922368
if (BsonUtils.supportsBson(source)) {
2293-
return (S) documentConverter.convert(this, BsonUtils.asBson(source), typeHint);
2369+
return (S) documentConverter.convert(context, BsonUtils.asBson(source), typeHint);
22942370
}
22952371

22962372
return (S) elementConverter.convert(source, typeHint);
22972373
}
22982374

2375+
@Override
2376+
public CustomConversions getCustomConversions() {
2377+
return conversions;
2378+
}
2379+
2380+
@Override
2381+
public MongoConverter getSourceConverter() {
2382+
return sourceConverter;
2383+
}
2384+
22992385
/**
2300-
* Create a new {@link ConversionContext} with {@link ObjectPath currentPath} applied.
2386+
* Create a new {@link DefaultConversionContext} with {@link ObjectPath currentPath} applied.
23012387
*
23022388
* @param currentPath must not be {@literal null}.
2303-
* @return a new {@link ConversionContext} with {@link ObjectPath currentPath} applied.
2389+
* @return a new {@link DefaultConversionContext} with {@link ObjectPath currentPath} applied.
23042390
*/
2305-
public ConversionContext withPath(ObjectPath currentPath) {
2391+
public DefaultConversionContext withPath(ObjectPath currentPath) {
23062392

23072393
Assert.notNull(currentPath, "ObjectPath must not be null");
23082394

2309-
return new ConversionContext(sourceConverter, conversions, currentPath, documentConverter, collectionConverter,
2310-
mapConverter, dbRefConverter, elementConverter);
2395+
return new DefaultConversionContext(sourceConverter, conversions, currentPath, documentConverter,
2396+
collectionConverter, mapConverter, dbRefConverter, elementConverter);
23112397
}
23122398

23132399
public ObjectPath getPath() {
23142400
return path;
23152401
}
23162402

2317-
public ConversionContext forProperty(String name) {
2403+
public DefaultConversionContext forProperty(String name) {
23182404
return this;
23192405
}
23202406

Diff for: spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java

+19-5
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
import org.mockito.Mock;
4545
import org.mockito.Mockito;
4646
import org.mockito.junit.jupiter.MockitoExtension;
47-
4847
import org.springframework.aop.framework.ProxyFactory;
4948
import org.springframework.beans.ConversionNotSupportedException;
5049
import org.springframework.beans.factory.annotation.Autowired;
@@ -2850,6 +2849,15 @@ public org.bson.Document write(@Nullable String domainValue, MongoConversionCont
28502849
assertThat(read.viaRegisteredConverter).isEqualTo("spring");
28512850
}
28522851

2852+
@Test // GH-4098
2853+
void resolvesCyclicNonAssociationValueFromSource/* and does not attempt to be smart and look up id values in context */() {
2854+
2855+
org.bson.Document source = new org.bson.Document("_id", "id-1").append("value", "v1").append("cycle",
2856+
new org.bson.Document("_id", "id-1").append("value", "v2"));
2857+
2858+
assertThat(converter.read(Cyclic.class, source).cycle.value).isEqualTo("v2");
2859+
}
2860+
28532861
static class GenericType<T> {
28542862
T content;
28552863
}
@@ -3573,12 +3581,10 @@ static class WithFieldWrite {
35733581
@org.springframework.data.mongodb.core.mapping.Field(
35743582
write = org.springframework.data.mongodb.core.mapping.Field.Write.ALWAYS) Integer writeAlways;
35753583

3576-
@org.springframework.data.mongodb.core.mapping.DBRef
3577-
@org.springframework.data.mongodb.core.mapping.Field(
3584+
@org.springframework.data.mongodb.core.mapping.DBRef @org.springframework.data.mongodb.core.mapping.Field(
35783585
write = org.springframework.data.mongodb.core.mapping.Field.Write.NON_NULL) Person writeNonNullPerson;
35793586

3580-
@org.springframework.data.mongodb.core.mapping.DBRef
3581-
@org.springframework.data.mongodb.core.mapping.Field(
3587+
@org.springframework.data.mongodb.core.mapping.DBRef @org.springframework.data.mongodb.core.mapping.Field(
35823588
write = org.springframework.data.mongodb.core.mapping.Field.Write.ALWAYS) Person writeAlwaysPerson;
35833589

35843590
}
@@ -3692,4 +3698,12 @@ static class Author {
36923698

36933699
}
36943700

3701+
@Data
3702+
static class Cyclic {
3703+
3704+
@Id String id;
3705+
String value;
3706+
Cyclic cycle;
3707+
}
3708+
36953709
}

0 commit comments

Comments
 (0)