From 54f2d278a4de1757223a8cb5e20618b6040bfa93 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 19 Mar 2020 15:14:16 +0100 Subject: [PATCH] DATACASS-741 - Add support for @Value. The `@Value` annotation can now be used on constructor parameters to evaluate expressions to obtain constructor values. The root object refers to a Row, UdtValue or TupleValue depending from which source the object gets materialized. class WithValue { final @Id String id; final @Transient String firstname; public WithValue(String id, @Value("#root.getString(1)") String firstname) { this.id = id; this.firstname = firstname; } } --- .../core/convert/CassandraValueProvider.java | 8 +++ .../convert/MappingCassandraConverter.java | 70 +++++++++++++++++-- .../core/convert/RowValueProvider.java | 8 +++ .../core/convert/TupleValueProvider.java | 8 +++ .../core/convert/UdtValueProvider.java | 8 +++ ...ssandraConverterTupleIntegrationTests.java | 33 +++++++++ .../MappingCassandraConverterUnitTests.java | 27 ++++++- src/main/asciidoc/reference/mapping.adoc | 1 + 8 files changed, 156 insertions(+), 7 deletions(-) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraValueProvider.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraValueProvider.java index 42884ac9a..15d068284 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraValueProvider.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraValueProvider.java @@ -35,4 +35,12 @@ public interface CassandraValueProvider extends PropertyValueProvider S readEntityFromRow(CassandraPersistentEntity entity, Row row) { @@ -286,12 +289,21 @@ public class MappingCassandraConverter extends AbstractCassandraConverter private S doReadEntity(CassandraPersistentEntity entity, CassandraValueProvider valueProvider) { - PersistentEntityParameterValueProvider parameterValueProvider = newParameterValueProvider( - entity, valueProvider); + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); + ParameterValueProvider provider; + + if (persistenceConstructor != null && persistenceConstructor.hasParameters()) { + SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(valueProvider.getSource(), spELContext); + PersistentEntityParameterValueProvider parameterValueProvider = newParameterValueProvider( + entity, valueProvider); + provider = new ConverterAwareSpELExpressionParameterValueProvider(evaluator, getConversionService(), + parameterValueProvider); + } else { + provider = NoOpParameterValueProvider.INSTANCE; + } EntityInstantiator instantiator = this.instantiators.getInstantiatorFor(entity); - - S instance = instantiator.createInstance(entity, parameterValueProvider); + S instance = instantiator.createInstance(entity, provider); if (entity.requiresPropertyPopulation()) { ConvertingPropertyAccessor propertyAccessor = newConvertingPropertyAccessor(instance, entity); @@ -1011,6 +1023,46 @@ public class MappingCassandraConverter extends AbstractCassandraConverter return Map.class.isAssignableFrom(mapType) ? mapType : Map.class; } + enum NoOpParameterValueProvider implements ParameterValueProvider { + + INSTANCE; + + @Override + public T getParameterValue(Parameter parameter) { + return null; + } + } + + /** + * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw + * resolved SpEL value. + */ + private class ConverterAwareSpELExpressionParameterValueProvider + extends SpELExpressionParameterValueProvider { + + /** + * Creates a new {@link ConverterAwareSpELExpressionParameterValueProvider}. + * + * @param evaluator must not be {@literal null}. + * @param conversionService must not be {@literal null}. + * @param delegate must not be {@literal null}. + */ + public ConverterAwareSpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator, + ConversionService conversionService, ParameterValueProvider delegate) { + + super(evaluator, conversionService, delegate); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.SpELExpressionParameterValueProvider#potentiallyConvertSpelValue(java.lang.Object, org.springframework.data.mapping.PreferredConstructor.Parameter) + */ + @Override + protected T potentiallyConvertSpelValue(Object object, Parameter parameter) { + return (T) convertReadValue(object, parameter.getType()); + } + } + /** * {@link CassandraRowValueProvider} that delegates reads to {@link CassandraValueProvider} applying mapping and * custom conversion from {@link MappingCassandraConverter}. @@ -1040,5 +1092,13 @@ public class MappingCassandraConverter extends AbstractCassandraConverter public T getPropertyValue(CassandraPersistentProperty property) { return (T) getReadValue(this.parent, property); } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.convert.CassandraValueProvider#getSource() + */ + @Override + public Object getSource() { + return parent.getSource(); + } } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/RowValueProvider.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/RowValueProvider.java index 109e4a647..ac540403d 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/RowValueProvider.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/RowValueProvider.java @@ -76,4 +76,12 @@ public class RowValueProvider implements CassandraValueProvider { return this.reader.contains(property.getRequiredColumnName()); } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.convert.CassandraValueProvider#getSource() + */ + @Override + public Object getSource() { + return this.reader.getRow(); + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/TupleValueProvider.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/TupleValueProvider.java index 41a0e29a5..77d532779 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/TupleValueProvider.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/TupleValueProvider.java @@ -81,4 +81,12 @@ public class TupleValueProvider implements CassandraValueProvider { public boolean hasProperty(CassandraPersistentProperty property) { return this.tupleValue.getType().getComponentTypes().size() >= property.getRequiredOrdinal(); } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.convert.CassandraValueProvider#getSource() + */ + @Override + public Object getSource() { + return this.tupleValue; + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/UdtValueProvider.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/UdtValueProvider.java index 2b87b81e4..addf61e06 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/UdtValueProvider.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/UdtValueProvider.java @@ -71,4 +71,12 @@ public class UdtValueProvider implements CassandraValueProvider { public boolean hasProperty(CassandraPersistentProperty property) { return this.udtValue.getType().contains(property.getRequiredColumnName()); } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.convert.CassandraValueProvider#getSource() + */ + @Override + public Object getSource() { + return this.udtValue; + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterTupleIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterTupleIntegrationTests.java index 81016778e..19f6f14b7 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterTupleIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterTupleIntegrationTests.java @@ -31,9 +31,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; import org.springframework.data.cassandra.config.SchemaAction; import org.springframework.data.cassandra.core.StatementFactory; import org.springframework.data.cassandra.core.cql.WriteOptions; @@ -113,6 +115,7 @@ public class MappingCassandraConverterTupleIntegrationTests extends AbstractSpri + "mapoftuples map, text>>>, " // + "mapoftuplevalues map>>, " // + "mappedtuple frozen, text>>, " // + + "mappedtuplewithvalue frozen, text>>, " // + "mappedtuples list, text>>>, " // + "PRIMARY KEY (id));"; this.session.execute(ddl); @@ -238,6 +241,20 @@ public class MappingCassandraConverterTupleIntegrationTests extends AbstractSpri assertThat(mappedTuple.getCurrency()).containsSequence(Currency.getInstance("EUR"), Currency.getInstance("USD")); } + @Test // DATACASS-741 + public void shouldReadTupleWithValue() { + + this.session.execute("INSERT INTO person (id,mappedtuplewithvalue) VALUES (" + "'foo'," // + + "({zip:'myzip'},['EUR','USD'],'bar'));\n"); + + ResultSet resultSet = this.session.execute("SELECT * FROM person;"); + + Person person = this.converter.read(Person.class, resultSet.one()); + + MappedTupleWithValue mappedTuple = person.getMappedTupleWithValue(); + assertThat(mappedTuple.myName).isEqualTo("bar"); + } + @Data @Table static class Person { @@ -246,6 +263,7 @@ public class MappingCassandraConverterTupleIntegrationTests extends AbstractSpri TupleValue tupleValue; MappedTuple mappedTuple; + MappedTupleWithValue mappedTupleWithValue; List mappedTuples; Map mapOfTuples; Map mapOfTupleValues; @@ -260,6 +278,21 @@ public class MappingCassandraConverterTupleIntegrationTests extends AbstractSpri @Element(2) String name; } + @Tuple + static class MappedTupleWithValue { + + final @Element(0) AddressUserType addressUserType; + final @Element(1) List currency; + final @Transient String myName; + + public MappedTupleWithValue(AddressUserType addressUserType, List currency, + @Value("#root.getString(2)") String myName) { + this.addressUserType = addressUserType; + this.currency = currency; + this.myName = myName; + } + } + @UserDefinedType("address") @Data static class AddressUserType { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java index d63c0a5d6..eb91eeb08 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java @@ -21,7 +21,6 @@ import static org.springframework.data.cassandra.test.util.RowMockUtil.*; import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; -import lombok.Value; import java.io.Serializable; import java.math.BigDecimal; @@ -39,6 +38,7 @@ import java.util.*; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Transient; @@ -958,6 +958,18 @@ public class MappingCassandraConverterUnitTests { .doesNotContainKey(CqlIdentifier.fromCql("computedName")); } + @Test // DATACASS-741 + public void shouldComputeValueInConstructor() { + + rowMock = RowMockUtil.newRowMock(RowMockUtil.column("id", "id", DataTypes.TEXT), + RowMockUtil.column("fn", "fn", DataTypes.TEXT)); + + WithValue result = this.mappingCassandraConverter.read(WithValue.class, rowMock); + + assertThat(result.id).isEqualTo("id"); + assertThat(result.firstname).isEqualTo("fn"); + } + private static List getValues(Map statement) { return new ArrayList<>(statement.values()); } @@ -1036,7 +1048,7 @@ public class MappingCassandraConverterUnitTests { @PrimaryKeyClass @RequiredArgsConstructor - @Value + @lombok.Value public static class EnumAndDateCompositePrimaryKey implements Serializable { @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED) private final Condition condition; @@ -1181,4 +1193,15 @@ public class MappingCassandraConverterUnitTests { @Transient String displayName; @ReadOnlyProperty String computedName; } + + static class WithValue { + + final @Id String id; + final @Transient String firstname; + + public WithValue(String id, @Value("#root.getString(1)") String firstname) { + this.id = id; + this.firstname = firstname; + } + } } diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 87430ec63..d77af62b3 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -355,6 +355,7 @@ to indicate either a single or a composite (compound) primary key. If used on a * `@PrimaryKeyClass`: Applied at the class level to indicate that this class is a compound primary key class. Must be referenced with `@PrimaryKey` in the entity class. * `@Transient`: By default, all private fields are mapped to the row. This annotation excludes the field +* `@Value`: This annotation is part of the Spring Framework . Within the mapping framework it can be applied to constructor arguments. This lets you use a Spring Expression Language statement to transform a key's value retrieved in the database before it is used to construct a domain object. In order to reference a property of a given `Row`/`UdtValue`/`TupleValue` one has to use expressions like: `@Value("#root.getString(0)")` where `root` refers to the root of the given document. * `@ReadOnlyProperty`: Applies at the field level to mark a property as read-only. Entity-bound insert and update statements do not include this property. where it is applied from being stored in the database. * `@Column`: Applied at the field level. Describes the column name as it is represented in the Cassandra table,