Create a new conversion context for projection properties.

We now create a new conversion context to ensure that we use the correct property type to avoid type retention when mapping complex objects within a projection.

Closes #3998
This commit is contained in:
Mark Paluch
2022-03-16 16:28:26 +01:00
parent 05730ded1b
commit 1009491920
2 changed files with 79 additions and 11 deletions

View File

@@ -39,7 +39,16 @@ 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.*;
import org.springframework.data.mapping.AccessOptions;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.Parameter;
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.callback.EntityCallbacks;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
@@ -385,7 +394,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
EntityProjection<?, ?> property = returnedTypeDescriptor.findProperty(name);
if (property == null) {
return super.forProperty(name);
return new ConversionContext(conversions, path, MappingMongoConverter.this::readDocument, collectionConverter,
mapConverter, dbRefConverter, elementConverter);
}
return new ProjectingConversionContext(conversions, path, collectionConverter, mapConverter, dbRefConverter,
@@ -1902,12 +1912,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
public MongoDbPropertyValueProvider withContext(ConversionContext context) {
if (context == this.context) {
return this;
}
return new MongoDbPropertyValueProvider(context, accessor, evaluator);
return context == this.context ? this : new MongoDbPropertyValueProvider(context, accessor, evaluator);
}
}

View File

@@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.DocumentTestUtils.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -2664,8 +2665,8 @@ class MappingMongoConverterUnitTests {
.and((target, underlyingType) -> !converter.conversions.isSimpleType(target)),
mappingContext);
EntityProjection<WithNestedProjection, Person> projection = introspector
.introspect(WithNestedProjection.class, Person.class);
EntityProjection<WithNestedProjection, Person> projection = introspector.introspect(WithNestedProjection.class,
Person.class);
WithNestedProjection person = converter.project(projection, source);
assertThat(person.getAddresses()).extracting(AddressProjection::getStreet).hasSize(1).containsOnly("hwy");
@@ -2689,6 +2690,22 @@ class MappingMongoConverterUnitTests {
assertThat(person.getAddresses()).extracting(Address::getStreet).hasSize(1).containsOnly("hwy");
}
@Test // GH-3998
void shouldReadOpenProjection() {
org.bson.Document author = new org.bson.Document("firstName", "Walter").append("lastName", "White");
org.bson.Document book = new org.bson.Document("_id", "foo").append("name", "my-book").append("author", author);
EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(),
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
.and((target, underlyingType) -> !converter.conversions.isSimpleType(target)),
mappingContext);
BookProjection projection = converter.project(introspector.introspect(BookProjection.class, Book.class), book);
assertThat(projection.getName()).isEqualTo("my-book by Walter White");
}
static class GenericType<T> {
T content;
}
@@ -3413,11 +3430,56 @@ class MappingMongoConverterUnitTests {
@org.springframework.data.mongodb.core.mapping.Field(
write = org.springframework.data.mongodb.core.mapping.Field.Write.ALWAYS) Integer writeAlways;
@org.springframework.data.mongodb.core.mapping.DBRef @org.springframework.data.mongodb.core.mapping.Field(
@org.springframework.data.mongodb.core.mapping.DBRef
@org.springframework.data.mongodb.core.mapping.Field(
write = org.springframework.data.mongodb.core.mapping.Field.Write.NON_NULL) Person writeNonNullPerson;
@org.springframework.data.mongodb.core.mapping.DBRef @org.springframework.data.mongodb.core.mapping.Field(
@org.springframework.data.mongodb.core.mapping.DBRef
@org.springframework.data.mongodb.core.mapping.Field(
write = org.springframework.data.mongodb.core.mapping.Field.Write.ALWAYS) Person writeAlwaysPerson;
}
interface BookProjection {
@Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}")
String getName();
}
@Data
static class Book {
@Id String id;
String name;
Author author = new Author();
public Book() {}
public Book(String id, String name, Author author) {
this.id = id;
this.name = name;
this.author = author;
}
}
static class Author {
@Id String id;
String firstName;
String lastName;
public Author() {}
public Author(String id, String firstName, String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}
}
}