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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user