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 #1240
This commit is contained in:
Mark Paluch
2022-03-16 16:45:27 +01:00
parent 97e2368b20
commit d670af5ec0
2 changed files with 76 additions and 12 deletions

View File

@@ -27,6 +27,7 @@ import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.ApplicationContext;
@@ -39,7 +40,16 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.cassandra.core.mapping.*;
import org.springframework.data.cassandra.core.mapping.Embedded.OnEmpty;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mapping.*;
import org.springframework.data.mapping.AccessOptions;
import org.springframework.data.mapping.InstanceCreatorMetadata;
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.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
@@ -1054,8 +1064,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class<?> target) {
if (value == null || target == null
|| ClassUtils.isAssignableValue(target, value)) {
if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) {
return value;
}
@@ -1308,10 +1317,9 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
final ValueConverter<Object> elementConverter;
public ConversionContext(org.springframework.data.convert.CustomConversions conversions,
ContainerValueConverter<Row> rowConverter,
ContainerValueConverter<TupleValue> tupleConverter, ContainerValueConverter<UdtValue> udtConverter,
ContainerValueConverter<Collection<?>> collectionConverter, ContainerValueConverter<Map<?, ?>> mapConverter,
ValueConverter<Object> elementConverter) {
ContainerValueConverter<Row> rowConverter, ContainerValueConverter<TupleValue> tupleConverter,
ContainerValueConverter<UdtValue> udtConverter, ContainerValueConverter<Collection<?>> collectionConverter,
ContainerValueConverter<Map<?, ?>> mapConverter, ValueConverter<Object> elementConverter) {
this.conversions = conversions;
this.rowConverter = rowConverter;
this.tupleConverter = tupleConverter;
@@ -1577,7 +1585,9 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
EntityProjection<?, ?> property = projection.findProperty(name);
if (property == null) {
return super.forProperty(name);
return new ConversionContext(conversions, MappingCassandraConverter.this::doReadRow,
MappingCassandraConverter.this::doReadTupleValue, MappingCassandraConverter.this::doReadUdtValue,
collectionConverter, mapConverter, elementConverter);
}
return new ProjectingConversionContext(conversions, rowConverter, tupleConverter, udtConverter,

View File

@@ -60,6 +60,7 @@ import org.springframework.data.cassandra.domain.TypeWithKeyClass;
import org.springframework.data.cassandra.domain.TypeWithMapId;
import org.springframework.data.cassandra.domain.User;
import org.springframework.data.cassandra.domain.UserToken;
import org.springframework.data.cassandra.support.UserDefinedTypeBuilder;
import org.springframework.data.cassandra.test.util.RowMockUtil;
import org.springframework.data.projection.EntityProjection;
import org.springframework.data.projection.EntityProjectionIntrospector;
@@ -67,6 +68,7 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.cql.Row;
import com.datastax.oss.driver.api.core.data.UdtValue;
import com.datastax.oss.driver.api.core.type.DataTypes;
import com.datastax.oss.driver.internal.core.data.DefaultTupleValue;
import com.datastax.oss.driver.internal.core.type.DefaultTupleType;
@@ -1308,9 +1310,8 @@ public class MappingCassandraConverterUnitTests {
@CassandraType(type = CassandraType.Name.SET,
typeArguments = CassandraType.Name.INT) private Set<Condition> conditionSet;
@CassandraType(type = CassandraType.Name.MAP,
typeArguments = { CassandraType.Name.INT,
CassandraType.Name.INT }) private Map<Condition, Condition> conditionMap;
@CassandraType(type = CassandraType.Name.MAP, typeArguments = { CassandraType.Name.INT,
CassandraType.Name.INT }) private Map<Condition, Condition> conditionMap;
}
@@ -1481,7 +1482,8 @@ public class MappingCassandraConverterUnitTests {
Row source = RowMockUtil.newRowMock(column("id", "id-1", DataTypes.TEXT), column("prefixage", 30, DataTypes.INT),
column("prefixfirstname", "fn", DataTypes.TEXT));
WithPrefixedNullableEmbeddedType target = mappingCassandraConverter.read(WithPrefixedNullableEmbeddedType.class, source);
WithPrefixedNullableEmbeddedType target = mappingCassandraConverter.read(WithPrefixedNullableEmbeddedType.class,
source);
assertThat(target.nested).isEqualTo(new EmbeddedWithSimpleTypes("fn", 30, null));
}
@@ -1514,6 +1516,29 @@ public class MappingCassandraConverterUnitTests {
assertThat(target.theJson.get("hello")).isEqualTo("world");
}
@Test // GH-1240
void shouldReadOpenProjectionWithNestedObject() {
com.datastax.oss.driver.api.core.type.UserDefinedType authorType = UserDefinedTypeBuilder.forName("author")
.withField("firstName", DataTypes.TEXT).withField("lastName", DataTypes.TEXT).build();
UdtValue udtValue = authorType.newValue().setString("firstName", "Walter").setString("lastName", "White");
Row source = RowMockUtil.newRowMock(column("id", "id-1", DataTypes.TEXT), column("name", "my-book", DataTypes.INT),
column("author", udtValue, authorType));
EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(
mappingCassandraConverter.getProjectionFactory(),
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
.and((target, underlyingType) -> !mappingCassandraConverter.getCustomConversions().isSimpleType(target)),
mappingContext);
BookProjection projection = mappingCassandraConverter
.project(introspector.introspect(BookProjection.class, Book.class), source);
assertThat(projection.getName()).isEqualTo("my-book by Walter White");
}
static class TypeWithJsonObject {
JSONObject theJson;
@@ -1542,4 +1567,33 @@ public class MappingCassandraConverterUnitTests {
}
}
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();
}
@Data
@UserDefinedType
static class Author {
@Id String id;
String firstName;
String lastName;
}
}