From f75d692221d04e5bfb7dbd24eeb5e7481a419b03 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 30 Mar 2021 14:14:32 +0200 Subject: [PATCH] Consider @Column and @Element annotated constructor parameters. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now allow using Column and Element annotations for constructor arguments when using constructor creation from columns/UDT fields respective tuple value elements. The column name/element ordinal can be overwritten for persistent properties. Non-persistent properties can be easier obtained than through the Value("…") annotation that uses SpEL. Closes #1104. --- ...AnnotatedCassandraConstructorProperty.java | 291 ++++++++++++++++++ .../convert/CassandraConstructorProperty.java | 283 +++++++++++++++++ .../convert/MappingCassandraConverter.java | 138 +++++---- .../data/cassandra/core/mapping/Column.java | 3 +- .../data/cassandra/core/mapping/Element.java | 2 +- .../MappingCassandraConverterUnitTests.java | 77 ++++- src/main/asciidoc/new-features.adoc | 1 + src/main/asciidoc/reference/mapping.adoc | 2 + 8 files changed, 725 insertions(+), 72 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/AnnotatedCassandraConstructorProperty.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraConstructorProperty.java diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/AnnotatedCassandraConstructorProperty.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/AnnotatedCassandraConstructorProperty.java new file mode 100644 index 000000000..c8bc66954 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/AnnotatedCassandraConstructorProperty.java @@ -0,0 +1,291 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.core.convert; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.data.cassandra.core.cql.Ordering; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; +import org.springframework.data.cassandra.core.mapping.Column; +import org.springframework.data.cassandra.core.mapping.Element; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +import com.datastax.oss.driver.api.core.CqlIdentifier; + +/** + * {@link CassandraPersistentProperty} wrapper for a delegate {@link CassandraPersistentProperty} considering + * annotations from a constructor parameter. + * + * @author Mark Paluch + * @since 3.2 + */ +class AnnotatedCassandraConstructorProperty implements CassandraPersistentProperty { + + private final CassandraPersistentProperty delegate; + + private final MergedAnnotations annotations; + + public AnnotatedCassandraConstructorProperty(CassandraPersistentProperty delegate, MergedAnnotations annotations) { + this.delegate = delegate; + this.annotations = annotations; + } + + @Override + @Nullable + public CqlIdentifier getColumnName() { + + if (annotations.isPresent(Column.class)) { + return CqlIdentifier.fromCql(annotations.get(Column.class).getString("value")); + } + + return delegate.getColumnName(); + } + + @Override + @Nullable + public Integer getOrdinal() { + + if (annotations.isPresent(Element.class)) { + return annotations.get(Element.class).getInt("value"); + } + + return delegate.getOrdinal(); + } + + @Override + @Nullable + public Ordering getPrimaryKeyOrdering() { + return delegate.getPrimaryKeyOrdering(); + } + + @Override + public boolean isClusterKeyColumn() { + return delegate.isClusterKeyColumn(); + } + + @Override + public boolean isCompositePrimaryKey() { + return delegate.isCompositePrimaryKey(); + } + + @Override + public boolean isMapLike() { + return delegate.isMapLike(); + } + + @Override + public boolean isPartitionKeyColumn() { + return delegate.isPartitionKeyColumn(); + } + + @Override + public boolean isPrimaryKeyColumn() { + return delegate.isPrimaryKeyColumn(); + } + + @Override + @Nullable + public AnnotatedType findAnnotatedType(Class annotationType) { + return delegate.findAnnotatedType(annotationType); + } + + @Override + public PersistentEntity getOwner() { + return delegate.getOwner(); + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public Class getType() { + return delegate.getType(); + } + + @Override + public TypeInformation getTypeInformation() { + return delegate.getTypeInformation(); + } + + @Override + public Iterable> getPersistentEntityTypes() { + return delegate.getPersistentEntityTypes(); + } + + @Override + @Nullable + public Method getGetter() { + return delegate.getGetter(); + } + + @Override + @Nullable + public Method getSetter() { + return delegate.getSetter(); + } + + @Override + @Nullable + public Method getWither() { + return delegate.getWither(); + } + + @Override + @Nullable + public Field getField() { + return delegate.getField(); + } + + @Override + @Nullable + public String getSpelExpression() { + return delegate.getSpelExpression(); + } + + @Override + @Nullable + public Association getAssociation() { + return delegate.getAssociation(); + } + + @Override + public boolean isEntity() { + return delegate.isEntity(); + } + + @Override + public boolean isIdProperty() { + return delegate.isIdProperty(); + } + + @Override + public boolean isVersionProperty() { + return delegate.isVersionProperty(); + } + + @Override + public boolean isCollectionLike() { + return delegate.isCollectionLike(); + } + + @Override + public boolean isMap() { + return delegate.isMap(); + } + + @Override + public boolean isArray() { + return delegate.isArray(); + } + + @Override + public boolean isTransient() { + return delegate.isTransient(); + } + + @Override + public boolean isWritable() { + return delegate.isWritable(); + } + + @Override + public boolean isImmutable() { + return delegate.isImmutable(); + } + + @Override + public boolean isAssociation() { + return delegate.isAssociation(); + } + + @Override + @Nullable + public Class getComponentType() { + return delegate.getComponentType(); + } + + @Override + public Class getRawType() { + return delegate.getRawType(); + } + + @Override + @Nullable + public Class getMapValueType() { + return delegate.getMapValueType(); + } + + @Override + public Class getActualType() { + return delegate.getActualType(); + } + + @Override + @Nullable + public A findAnnotation(Class annotationType) { + return delegate.findAnnotation(annotationType); + } + + @Override + @Nullable + public A findPropertyOrOwnerAnnotation(Class annotationType) { + return delegate.findPropertyOrOwnerAnnotation(annotationType); + } + + @Override + public boolean isAnnotationPresent(Class annotationType) { + return delegate.isAnnotationPresent(annotationType); + } + + @Override + public boolean usePropertyAccess() { + return delegate.usePropertyAccess(); + } + + @Override + @Nullable + public Class getAssociationTargetType() { + return delegate.getAssociationTargetType(); + } + + @Override + public void setColumnName(CqlIdentifier columnName) { + throw new UnsupportedOperationException(); + } + + @Override + @Deprecated + public void setForceQuote(boolean forceQuote) { + throw new UnsupportedOperationException(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + throw new UnsupportedOperationException(); + } + +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraConstructorProperty.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraConstructorProperty.java new file mode 100644 index 000000000..5b4aad197 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraConstructorProperty.java @@ -0,0 +1,283 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.core.convert; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Optional; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.data.cassandra.core.cql.Ordering; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +import com.datastax.oss.driver.api.core.CqlIdentifier; + +/** + * Annotated {@link CassandraPersistentProperty} synthesized from a constructor parameter. + * + * @author Mark Paluch + * @since 3.2 + */ +class CassandraConstructorProperty implements CassandraPersistentProperty { + + private final String name; + + private final CassandraPersistentEntity owner; + + private final TypeInformation typeInformation; + + public CassandraConstructorProperty(String name, CassandraPersistentEntity owner, + TypeInformation typeInformation) { + this.name = name; + this.owner = owner; + this.typeInformation = typeInformation; + } + + @Nullable + @Override + public CqlIdentifier getColumnName() { + throw new IllegalStateException(String.format("Parameter %s is not annotated with @Column", name)); + } + + @Nullable + @Override + public Integer getOrdinal() { + throw new IllegalStateException(String.format("Parameter %s is not annotated with @Element", name)); + } + + @Nullable + @Override + public Ordering getPrimaryKeyOrdering() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isClusterKeyColumn() { + return false; + } + + @Override + public boolean isCompositePrimaryKey() { + return false; + } + + @Override + public boolean isMapLike() { + return typeInformation.isMap(); + } + + @Override + public boolean isPartitionKeyColumn() { + return false; + } + + @Override + public boolean isPrimaryKeyColumn() { + return false; + } + + @Nullable + @Override + public AnnotatedType findAnnotatedType(Class annotationType) { + return null; + } + + @Override + public PersistentEntity getOwner() { + return owner; + } + + @Override + public String getName() { + return name; + } + + @Override + public Class getType() { + return null; + } + + @Override + public TypeInformation getTypeInformation() { + return typeInformation; + } + + @Override + public Iterable> getPersistentEntityTypes() { + return Collections.emptyList(); + } + + @Nullable + @Override + public Method getGetter() { + return null; + } + + @Nullable + @Override + public Method getSetter() { + return null; + } + + @Nullable + @Override + public Method getWither() { + return null; + } + + @Nullable + @Override + public Field getField() { + return null; + } + + @Nullable + @Override + public String getSpelExpression() { + return null; + } + + @Nullable + @Override + public Association getAssociation() { + return null; + } + + @Override + public boolean isEntity() { + return false; + } + + @Override + public boolean isIdProperty() { + return false; + } + + @Override + public boolean isVersionProperty() { + return false; + } + + @Override + public boolean isCollectionLike() { + return typeInformation.isCollectionLike(); + } + + @Override + public boolean isMap() { + return typeInformation.isMap(); + } + + @Override + public boolean isArray() { + return typeInformation.getType().isArray(); + } + + @Override + public boolean isTransient() { + return false; + } + + @Override + public boolean isWritable() { + return false; + } + + @Override + public boolean isImmutable() { + return false; + } + + @Override + public boolean isAssociation() { + return false; + } + + @Nullable + @Override + public Class getComponentType() { + return Optional.ofNullable(typeInformation.getComponentType()).map(TypeInformation::getType).orElse(null); + } + + @Override + public Class getRawType() { + return typeInformation.getType(); + } + + @Nullable + @Override + public Class getMapValueType() { + return Optional.ofNullable(typeInformation.getMapValueType()).map(TypeInformation::getType).orElse(null); + } + + @Override + public Class getActualType() { + return typeInformation.getRequiredActualType().getType(); + } + + @Nullable + @Override + public A findAnnotation(Class annotationType) { + return null; + } + + @Nullable + @Override + public A findPropertyOrOwnerAnnotation(Class annotationType) { + return null; + } + + @Override + public boolean isAnnotationPresent(Class annotationType) { + return false; + } + + @Override + public boolean usePropertyAccess() { + return false; + } + + @Nullable + @Override + public Class getAssociationTargetType() { + return null; + } + + @Override + public void setColumnName(CqlIdentifier columnName) { + throw new UnsupportedOperationException(); + } + + @Override + @Deprecated + public void setForceQuote(boolean forceQuote) { + throw new UnsupportedOperationException(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + throw new UnsupportedOperationException(); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java index 55dcf0a6f..b01614859 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java @@ -31,20 +31,12 @@ import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.CollectionFactory; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.cassandra.core.mapping.BasicCassandraPersistentEntity; -import org.springframework.data.cassandra.core.mapping.BasicMapId; -import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; -import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; -import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; -import org.springframework.data.cassandra.core.mapping.Embedded; +import org.springframework.data.cassandra.core.mapping.*; import org.springframework.data.cassandra.core.mapping.Embedded.OnEmpty; -import org.springframework.data.cassandra.core.mapping.EmbeddedEntityOperations; -import org.springframework.data.cassandra.core.mapping.MapId; -import org.springframework.data.cassandra.core.mapping.MapIdentifiable; -import org.springframework.data.cassandra.core.mapping.UserTypeResolver; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PreferredConstructor; @@ -54,7 +46,6 @@ 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; @@ -284,11 +275,10 @@ public class MappingCassandraConverter extends AbstractCassandraConverter return new ConvertingPropertyAccessor<>(propertyAccessor, getConversionService()); } - private PersistentEntityParameterValueProvider newParameterValueProvider( - ConversionContext context, CassandraPersistentEntity entity, CassandraValueProvider valueProvider) { + private CassandraPersistentEntityParameterValueProvider newParameterValueProvider(ConversionContext context, + CassandraPersistentEntity entity, CassandraValueProvider valueProvider) { - return new PersistentEntityParameterValueProvider<>(entity, - new MappingAndConvertingValueProvider(valueProvider, context), null); + return new CassandraPersistentEntityParameterValueProvider(entity, valueProvider, context, null); } @SuppressWarnings("unchecked") @@ -416,8 +406,8 @@ public class MappingCassandraConverter extends AbstractCassandraConverter if (persistenceConstructor != null && persistenceConstructor.hasParameters()) { SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(valueProvider.getSource(), spELContext); - PersistentEntityParameterValueProvider parameterValueProvider = newParameterValueProvider( - context, entity, valueProvider); + ParameterValueProvider parameterValueProvider = newParameterValueProvider(context, + entity, valueProvider); provider = new ConverterAwareSpELExpressionParameterValueProvider(evaluator, getConversionService(), parameterValueProvider, context); } else { @@ -1164,50 +1154,6 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } } - /** - * {@link CassandraRowValueProvider} that delegates reads to {@link CassandraValueProvider} applying mapping and - * custom conversion from {@link MappingCassandraConverter}. - * - * @author Mark Paluch - * @since 1.5.1 - */ - class MappingAndConvertingValueProvider implements CassandraValueProvider { - - private final CassandraValueProvider parent; - private final ConversionContext context; - - public MappingAndConvertingValueProvider(CassandraValueProvider parent, ConversionContext context) { - this.parent = parent; - this.context = context; - } - - /* (non-Javadoc) - * @see org.springframework.data.cassandra.core.convert.CassandraValueProvider#hasProperty(org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty) - */ - @Override - public boolean hasProperty(CassandraPersistentProperty property) { - return this.parent.hasProperty(property); - } - - /* (non-Javadoc) - * @see org.springframework.data.mapping.model.PropertyValueProvider#getPropertyValue(org.springframework.data.mapping.PersistentProperty) - */ - @Nullable - @Override - @SuppressWarnings("unchecked") - public T getPropertyValue(CassandraPersistentProperty property) { - return (T) getReadValue(context, this.parent, property); - } - - /* (non-Javadoc) - * @see org.springframework.data.cassandra.core.convert.CassandraValueProvider#getSource() - */ - @Override - public Object getSource() { - return parent.getSource(); - } - } - /** * Conversion context holding references to simple {@link ValueConverter} and {@link ContainerValueConverter}. * Entrypoint for recursive conversion of {@link Row} and other types. @@ -1311,4 +1257,74 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } } + + /** + * Cassandra-specific {@link ParameterValueProvider} considering {@link Column @Column} and {@link Element @Element} + * annotations on constructor parameters. + * + * @since 3.2 + */ + class CassandraPersistentEntityParameterValueProvider implements ParameterValueProvider { + + private final CassandraPersistentEntity entity; + private final CassandraValueProvider provider; + private final ConversionContext context; + private final @Nullable Object parent; + + public CassandraPersistentEntityParameterValueProvider(CassandraPersistentEntity entity, + CassandraValueProvider provider, ConversionContext context, @Nullable Object parent) { + this.entity = entity; + this.provider = provider; + this.context = context; + this.parent = parent; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter) + */ + @Nullable + @SuppressWarnings("unchecked") + public T getParameterValue(Parameter parameter) { + + PreferredConstructor, CassandraPersistentProperty> constructor = (PreferredConstructor, CassandraPersistentProperty>) entity + .getPersistenceConstructor(); + + if (constructor != null && constructor.isEnclosingClassParameter(parameter)) { + return (T) parent; + } + + String name = parameter.getName(); + + if (name == null) { + throw new MappingException(String.format("Parameter %s does not have a name!", parameter)); + } + + CassandraPersistentProperty property = getPersistentProperty(name, parameter.getType(), + parameter.getAnnotations()); + + if (property == null) { + + throw new MappingException(String.format("No property %s found on entity %s to bind constructor parameter to!", + name, entity.getType())); + } + + return (T) getReadValue(context, provider, property); + } + + @Nullable + private CassandraPersistentProperty getPersistentProperty(String name, TypeInformation typeInformation, + MergedAnnotations annotations) { + + CassandraPersistentProperty property = entity.getPersistentProperty(name); + + if (annotations.isPresent(Column.class) || annotations.isPresent(Element.class)) { + return new AnnotatedCassandraConstructorProperty( + property == null ? new CassandraConstructorProperty(name, entity, typeInformation) : property, annotations); + } + + return property; + } + } + } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/Column.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/Column.java index ae3a6959f..e09384f26 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/Column.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/Column.java @@ -41,10 +41,11 @@ import java.lang.annotation.Target; * * @author Alex Shvid * @author Matthew T. Adams + * @author Mark Paluch */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target(value = { ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD }) +@Target(value = { ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) public @interface Column { /** diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/Element.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/Element.java index c471ec212..6afed44bc 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/Element.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/Element.java @@ -34,7 +34,7 @@ import java.lang.annotation.Target; @Documented @Inherited @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.FIELD, ElementType.METHOD }) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) public @interface Element { /** 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 5f322548d..e8bbf9275 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 @@ -46,15 +46,8 @@ import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Transient; import org.springframework.data.cassandra.core.cql.PrimaryKeyType; -import org.springframework.data.cassandra.core.mapping.BasicMapId; -import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; -import org.springframework.data.cassandra.core.mapping.CassandraType; -import org.springframework.data.cassandra.core.mapping.Embedded; -import org.springframework.data.cassandra.core.mapping.MapId; -import org.springframework.data.cassandra.core.mapping.PrimaryKey; -import org.springframework.data.cassandra.core.mapping.PrimaryKeyClass; -import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn; -import org.springframework.data.cassandra.core.mapping.Table; +import org.springframework.data.cassandra.core.mapping.*; +import org.springframework.data.cassandra.core.mapping.Column; import org.springframework.data.cassandra.domain.AllPossibleTypes; import org.springframework.data.cassandra.domain.CompositeKey; import org.springframework.data.cassandra.domain.TypeWithCompositeKey; @@ -67,6 +60,8 @@ import org.springframework.data.cassandra.test.util.RowMockUtil; import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.cql.Row; 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; /** * Unit tests for {@link MappingCassandraConverter}. @@ -1014,6 +1009,39 @@ public class MappingCassandraConverterUnitTests { assertThat(insert).containsEntry(CqlIdentifier.fromCql("conditionmap"), Collections.singletonMap(0, 1)); } + @Test + void shouldConsiderColumnAnnotationOnConstructor() { + + rowMock = RowMockUtil.newRowMock(RowMockUtil.column("fn", "Walter", DataTypes.ASCII), + RowMockUtil.column("firstname", "Heisenberg", DataTypes.ASCII), + RowMockUtil.column("lastname", "White", DataTypes.ASCII)); + + WithColumnAnnotationInConstructor converted = this.mappingCassandraConverter + .read(WithColumnAnnotationInConstructor.class, rowMock); + + assertThat(converted.firstname).isEqualTo("Walter"); + assertThat(converted.lastname).isEqualTo("White"); + } + + @Test + void shouldConsiderElementAnnotationOnConstructor() { + + DefaultTupleValue value = new DefaultTupleValue( + new DefaultTupleType(Arrays.asList(DataTypes.ASCII, DataTypes.ASCII, DataTypes.ASCII))); + + value.setString(0, "Zero"); + value.setString(1, "One"); + value.setString(2, "Two"); + + rowMock = RowMockUtil.newRowMock(RowMockUtil.column("firstname", "Heisenberg", DataTypes.ASCII), + RowMockUtil.column("tuple", value, value.getType())); + + WithMappedTuple converted = this.mappingCassandraConverter.read(WithMappedTuple.class, rowMock); + + assertThat(converted.firstname).isEqualTo("Heisenberg"); + assertThat(converted.tuple.firstname).isEqualTo("Two"); + } + private static List getValues(Map statement) { return new ArrayList<>(statement.values()); } @@ -1263,6 +1291,37 @@ public class MappingCassandraConverterUnitTests { } } + private static class WithColumnAnnotationInConstructor { + + String firstname; + final @Transient String lastname; + + public WithColumnAnnotationInConstructor(@Column("fn") String firstname, @Column("lastname") String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + } + + private static class WithMappedTuple { + + String firstname; + TupleWithElementAnnotationInConstructor tuple; + } + + @Tuple + private static class TupleWithElementAnnotationInConstructor { + + @Element(0) String zero; + @Element(1) String one; + @Element(2) String two; + + @Transient String firstname; + + public TupleWithElementAnnotationInConstructor(@Element(2) String firstname) { + this.firstname = firstname; + } + } + @ToString static class WithNullableEmbeddedType { diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 81bf54bfa..3bf74f51e 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,6 +7,7 @@ This chapter summarizes changes and new features for each release. == What's new in Spring Data for Apache Cassandra 3.2 * <> using `CassandraTemplate` and repositories (enabled by default). +* `@Column` and `@Element` can be used on constructor arguments. [[new-features.3-1-0]] == What's new in Spring Data for Apache Cassandra 3.1 diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 9636fbd8b..8ef3a5899 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -432,6 +432,7 @@ In order to reference a property of a given `Row`/`UdtValue`/`TupleValue` one ha Entity-bound insert and update statements do not include this property. * `@Column`: Applied at the field level. Describes the column name as it is represented in the Cassandra table, thus letting the name differ from the field name of the class. +Can be used on constructor arguments to customize the column name during constructor creation. * `@Embedded`: Applied at the field level. Enables embedded object usage for types mapped to a table or a user-defined type. Properties of the embedded object are flattened into the structure of its parent. @@ -448,6 +449,7 @@ Types are derived from the declaration by default. * `@Tuple`: Applied at the type level to use a type as a mapped tuple. * `@Element`: Applied at the field level to specify element or field ordinals within a mapped tuple. Types are derived from the property declaration by default. +Can be used on constructor arguments to customize tuple element ordinals during constructor creation. * `@Version`: Applied at field level is used for optimistic locking and checked for modification on save operations. The initial value is `zero` which is bumped automatically on every update.