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;
    }
}
This commit is contained in:
Mark Paluch
2020-03-19 15:14:16 +01:00
parent 2a20fb8aa1
commit 54f2d278a4
8 changed files with 156 additions and 7 deletions

View File

@@ -35,4 +35,12 @@ public interface CassandraValueProvider extends PropertyValueProvider<CassandraP
* {@link CassandraPersistentProperty}.
*/
boolean hasProperty(CassandraPersistentProperty property);
/**
* Returns whether the underlying source.
*
* @return the underlying source for this {@link CassandraValueProvider}.
* @since 3.0
*/
Object getSource();
}

View File

@@ -46,13 +46,17 @@ import org.springframework.data.cassandra.core.mapping.MapId;
import org.springframework.data.cassandra.core.mapping.MapIdentifiable;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
import org.springframework.data.mapping.model.EntityInstantiator;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
@@ -257,7 +261,6 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
.getRequiredPersistentEntity(typeInfo);
return readEntityFromRow(persistentEntity, row);
}
private <S> S readEntityFromRow(CassandraPersistentEntity<S> entity, Row row) {
@@ -286,12 +289,21 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
private <S> S doReadEntity(CassandraPersistentEntity<S> entity, CassandraValueProvider valueProvider) {
PersistentEntityParameterValueProvider<CassandraPersistentProperty> parameterValueProvider = newParameterValueProvider(
entity, valueProvider);
PreferredConstructor<S, CassandraPersistentProperty> persistenceConstructor = entity.getPersistenceConstructor();
ParameterValueProvider<CassandraPersistentProperty> provider;
if (persistenceConstructor != null && persistenceConstructor.hasParameters()) {
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(valueProvider.getSource(), spELContext);
PersistentEntityParameterValueProvider<CassandraPersistentProperty> 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<S> 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<CassandraPersistentProperty> {
INSTANCE;
@Override
public <T> T getParameterValue(Parameter<T, CassandraPersistentProperty> parameter) {
return null;
}
}
/**
* Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw
* resolved SpEL value.
*/
private class ConverterAwareSpELExpressionParameterValueProvider
extends SpELExpressionParameterValueProvider<CassandraPersistentProperty> {
/**
* 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<CassandraPersistentProperty> 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> T potentiallyConvertSpelValue(Object object, Parameter<T, CassandraPersistentProperty> 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> 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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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, frozen<tuple<address, list<text>, text>>>, " //
+ "mapoftuplevalues map<text, frozen<tuple<text, int>>>, " //
+ "mappedtuple frozen<tuple<address, list<text>, text>>, " //
+ "mappedtuplewithvalue frozen<tuple<address, list<text>, text>>, " //
+ "mappedtuples list<frozen<tuple<address, list<text>, 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<MappedTuple> mappedTuples;
Map<String, MappedTuple> mapOfTuples;
Map<String, TupleValue> 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> currency;
final @Transient String myName;
public MappedTupleWithValue(AddressUserType addressUserType, List<Currency> currency,
@Value("#root.getString(2)") String myName) {
this.addressUserType = addressUserType;
this.currency = currency;
this.myName = myName;
}
}
@UserDefinedType("address")
@Data
static class AddressUserType {

View File

@@ -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<Object> getValues(Map<CqlIdentifier, Object> 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;
}
}
}

View File

@@ -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,