diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/BeanPropertyRowMapper.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/BeanPropertyRowMapper.java
new file mode 100644
index 000000000..53998c9bc
--- /dev/null
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/BeanPropertyRowMapper.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright 2020 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.cql;
+
+import java.beans.PropertyDescriptor;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.BeanWrapperImpl;
+import org.springframework.beans.NotWritablePropertyException;
+import org.springframework.beans.TypeConverter;
+import org.springframework.beans.TypeMismatchException;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.core.convert.support.DefaultConversionService;
+import org.springframework.dao.DataRetrievalFailureException;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.cassandra.core.convert.CassandraJsr310Converters;
+import org.springframework.data.convert.Jsr310Converters;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+
+import com.datastax.oss.driver.api.core.cql.Row;
+
+/**
+ * {@link RowMapper} implementation that converts a {@link Row} into a new instance of the specified mapped target
+ * class. The mapped target class must be a top-level class and it must have a default or no-arg constructor.
+ *
+ * Column values are mapped based on matching the column name as obtained from result set meta-data to public setters
+ * for the corresponding properties. The names are matched either directly or by transforming a name separating the
+ * parts with underscores to the same name using "camel" case.
+ *
+ * Mapping is provided for fields in the target class for many common types, e.g.: String, boolean, Boolean, byte, Byte,
+ * short, Short, int, Integer, long, Long, float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc.
+ *
+ * To facilitate mapping between columns and fields that don't have matching names, try using column aliases in the CQL
+ * statement like "select fname as first_name from customer".
+ *
+ * For 'null' values read from the database, we will attempt to call the setter, but in the case of Java primitives,
+ * this causes a {@link TypeMismatchException}. This class can be configured (using the
+ * {@code primitivesDefaultedForNullValue} property) to trap this exception and use the primitives default value. Be
+ * aware that if you use the values from the generated bean to update the database the primitive value will have been
+ * set to the primitive's default value instead of null.
+ *
+ * Please note that this class is designed to provide convenience rather than high performance. For best performance,
+ * consider using a custom {@link RowMapper} implementation.
+ *
+ * @author Mark Paluch
+ * @since 3.1
+ * @param the result type
+ */
+public class BeanPropertyRowMapper implements RowMapper {
+
+ /** Logger available to subclasses. */
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ /** The class we are mapping to. */
+ private @Nullable Class mappedClass;
+
+ /** Whether we're strictly validating. */
+ private boolean checkFullyPopulated = false;
+
+ /** Whether we're defaulting primitives when mapping a null value. */
+ private boolean primitivesDefaultedForNullValue = false;
+
+ /** ConversionService for binding values to bean properties. */
+ private @Nullable ConversionService conversionService;
+
+ /** Map of the fields we provide mapping for. */
+ private @Nullable Map mappedFields;
+
+ /** Set of bean properties we provide mapping for. */
+ private @Nullable Set mappedProperties;
+
+ /**
+ * Create a new {@code BeanPropertyRowMapper} for bean-style configuration.
+ *
+ * @see #setMappedClass
+ * @see #setCheckFullyPopulated
+ */
+ public BeanPropertyRowMapper() {}
+
+ /**
+ * Create a new {@code BeanPropertyRowMapper}, accepting unpopulated properties in the target bean.
+ *
+ * @param mappedClass the class that each row should be mapped to
+ */
+ public BeanPropertyRowMapper(Class mappedClass) {
+ setMappedClass(mappedClass);
+ }
+
+ /**
+ * Create a new {@code BeanPropertyRowMapper}.
+ *
+ * @param mappedClass the class that each row should be mapped to
+ * @param checkFullyPopulated whether we're strictly validating that all bean properties have been mapped from
+ * corresponding database fields
+ */
+ public BeanPropertyRowMapper(Class mappedClass, boolean checkFullyPopulated) {
+ initialize(mappedClass);
+ this.checkFullyPopulated = checkFullyPopulated;
+ }
+
+ {
+ DefaultConversionService conversionService = new DefaultConversionService();
+ Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter);
+ CassandraJsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter);
+ this.conversionService = conversionService;
+ }
+
+ /**
+ * Set the class that each row should be mapped to.
+ */
+ public void setMappedClass(Class mappedClass) {
+ if (this.mappedClass == null) {
+ initialize(mappedClass);
+ } else {
+ if (this.mappedClass != mappedClass) {
+ throw new InvalidDataAccessApiUsageException("The mapped class can not be reassigned to map to " + mappedClass
+ + " since it is already providing mapping for " + this.mappedClass);
+ }
+ }
+ }
+
+ /**
+ * Get the class that we are mapping to.
+ */
+ @Nullable
+ public final Class getMappedClass() {
+ return this.mappedClass;
+ }
+
+ /**
+ * Set whether we're strictly validating that all bean properties have been mapped from corresponding database fields.
+ *
+ * Default is {@literal false}, accepting unpopulated properties in the target bean.
+ */
+ public void setCheckFullyPopulated(boolean checkFullyPopulated) {
+ this.checkFullyPopulated = checkFullyPopulated;
+ }
+
+ /**
+ * Return whether we're strictly validating that all bean properties have been mapped from corresponding database
+ * fields.
+ */
+ public boolean isCheckFullyPopulated() {
+ return this.checkFullyPopulated;
+ }
+
+ /**
+ * Set whether we're defaulting Java primitives in the case of mapping a null value from corresponding database
+ * fields.
+ *
+ * Default is {@literal false}, throwing an exception when nulls are mapped to Java primitives.
+ */
+ public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) {
+ this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue;
+ }
+
+ /**
+ * Return whether we're defaulting Java primitives in the case of mapping a null value from corresponding database
+ * fields.
+ */
+ public boolean isPrimitivesDefaultedForNullValue() {
+ return this.primitivesDefaultedForNullValue;
+ }
+
+ /**
+ * Set a {@link ConversionService} for binding Cassandra values to bean properties, or {@literal null} for none.
+ *
+ * Default is a {@link DefaultConversionService}. This provides support for {@code java.time} conversion and other
+ * special types.
+ *
+ * @see #initBeanWrapper(BeanWrapper)
+ */
+ public void setConversionService(@Nullable ConversionService conversionService) {
+ this.conversionService = conversionService;
+ }
+
+ /**
+ * Return a {@link ConversionService} for binding Cassandra values to bean properties, or {@literal null} if none.
+ */
+ @Nullable
+ public ConversionService getConversionService() {
+ return this.conversionService;
+ }
+
+ /**
+ * Initialize the mapping meta-data for the given class.
+ *
+ * @param mappedClass the mapped class
+ */
+ protected void initialize(Class mappedClass) {
+ this.mappedClass = mappedClass;
+ this.mappedFields = new HashMap<>();
+ this.mappedProperties = new HashSet<>();
+
+ for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(mappedClass)) {
+ if (pd.getWriteMethod() != null) {
+ this.mappedFields.put(lowerCaseName(pd.getName()), pd);
+ String underscoredName = underscoreName(pd.getName());
+ if (!lowerCaseName(pd.getName()).equals(underscoredName)) {
+ this.mappedFields.put(underscoredName, pd);
+ }
+ this.mappedProperties.add(pd.getName());
+ }
+ }
+ }
+
+ /**
+ * Convert a name in camelCase to an underscored name in lower case. Any upper case letters are converted to lower
+ * case with a preceding underscore.
+ *
+ * @param name the original name
+ * @return the converted name
+ * @see #lowerCaseName
+ */
+ protected String underscoreName(String name) {
+ if (!StringUtils.hasLength(name)) {
+ return "";
+ }
+
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ if (Character.isUpperCase(c)) {
+ result.append('_').append(Character.toLowerCase(c));
+ } else {
+ result.append(c);
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Convert the given name to lower case. By default, conversions will happen within the US locale.
+ *
+ * @param name the original name
+ * @return the converted name
+ */
+ protected String lowerCaseName(String name) {
+ return name.toLowerCase(Locale.US);
+ }
+
+ /**
+ * Extract the values for all columns in the current row.
+ *
+ * Utilizes public setters and result set meta-data.
+ */
+ @Override
+ public T mapRow(Row row, int rowNumber) {
+
+ BeanWrapperImpl bw = new BeanWrapperImpl();
+ initBeanWrapper(bw);
+
+ T mappedObject = constructMappedInstance(row, bw);
+ bw.setBeanInstance(mappedObject);
+ int columnCount = row.getColumnDefinitions().size();
+ Set populatedProperties = (isCheckFullyPopulated() ? new HashSet<>() : null);
+
+ for (int index = 0; index < columnCount; index++) {
+ String column = row.getColumnDefinitions().get(index).getName().toString();
+ String field = lowerCaseName(StringUtils.delete(column, " "));
+ PropertyDescriptor pd = (this.mappedFields != null ? this.mappedFields.get(field) : null);
+ if (pd != null) {
+ try {
+ Object value = getColumnValue(row, index, pd);
+ if (rowNumber == 0 && logger.isDebugEnabled()) {
+ logger.debug("Mapping column '" + column + "' to property '" + pd.getName() + "' of type '"
+ + ClassUtils.getQualifiedName(pd.getPropertyType()) + "'");
+ }
+ try {
+ bw.setPropertyValue(pd.getName(), value);
+ } catch (TypeMismatchException ex) {
+ if (value == null && this.primitivesDefaultedForNullValue) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Intercepted TypeMismatchException for row " + rowNumber + " and column '" + column
+ + "' with null value when setting property '" + pd.getName() + "' of type '"
+ + ClassUtils.getQualifiedName(pd.getPropertyType()) + "' on object: " + mappedObject, ex);
+ }
+ } else {
+ throw ex;
+ }
+ }
+ if (populatedProperties != null) {
+ populatedProperties.add(pd.getName());
+ }
+ } catch (NotWritablePropertyException ex) {
+ throw new DataRetrievalFailureException(
+ "Unable to map column '" + column + "' to property '" + pd.getName() + "'", ex);
+ }
+ } else {
+ // No PropertyDescriptor found
+ if (rowNumber == 0 && logger.isDebugEnabled()) {
+ logger.debug("No property found for column '" + column + "' mapped to field '" + field + "'");
+ }
+ }
+ }
+
+ if (populatedProperties != null && !populatedProperties.equals(this.mappedProperties)) {
+ throw new InvalidDataAccessApiUsageException("Given Row does not contain all columns "
+ + "necessary to populate object of " + this.mappedClass + ": " + this.mappedProperties);
+ }
+
+ return mappedObject;
+ }
+
+ /**
+ * Construct an instance of the mapped class for the current row.
+ *
+ * @param row the row to map (pre-initialized for the current row)
+ * @param tc a TypeConverter with this RowMapper's conversion service
+ * @return a corresponding instance of the mapped class
+ */
+ protected T constructMappedInstance(Row row, TypeConverter tc) {
+
+ Assert.state(this.mappedClass != null, "Mapped class was not specified");
+
+ return BeanUtils.instantiateClass(this.mappedClass);
+ }
+
+ /**
+ * Initialize the given {@link BeanWrapper} to be used for row mapping. To be called for each row.
+ *
+ * The default implementation applies the configured {@link ConversionService}, if any. Can be overridden in
+ * subclasses.
+ *
+ * @param bw the BeanWrapper to initialize
+ * @see #getConversionService()
+ * @see BeanWrapper#setConversionService
+ */
+ protected void initBeanWrapper(BeanWrapper bw) {
+
+ ConversionService cs = getConversionService();
+
+ if (cs != null) {
+ bw.setConversionService(cs);
+ }
+ }
+
+ /**
+ * Retrieve a Cassandra object value for the specified column.
+ *
+ * The default implementation delegates to {@link Row#get(int, Class)}.
+ *
+ * @param row is the row holding the data
+ * @param index is the column index
+ * @param pd the bean property that each result object is expected to match
+ * @return the Object value
+ * @see #getColumnValue(Row, int, Class)
+ */
+ @Nullable
+ protected Object getColumnValue(Row row, int index, PropertyDescriptor pd) {
+ return row.get(index, pd.getPropertyType());
+ }
+
+ /**
+ * Retrieve a Cassandra object value for the specified column.
+ *
+ * The default implementation calls {@link Row#get(int, Class)}. Subclasses may override this to check specific value
+ * types upfront, or to post-process values return from {@code get}.
+ *
+ * @param row is the row holding the data.
+ * @param index is the column index.
+ * @param paramType the target parameter type.
+ * @return the Object value.
+ */
+ @Nullable
+ protected Object getColumnValue(Row row, int index, Class> paramType) {
+ return row.get(index, paramType);
+ }
+
+ /**
+ * Static factory method to create a new {@code BeanPropertyRowMapper}.
+ *
+ * @param mappedClass the class that each row should be mapped to.
+ * @see #newInstance(Class, ConversionService)
+ */
+ public static BeanPropertyRowMapper newInstance(Class mappedClass) {
+ return new BeanPropertyRowMapper<>(mappedClass);
+ }
+
+ /**
+ * Static factory method to create a new {@code BeanPropertyRowMapper}.
+ *
+ * @param mappedClass the class that each row should be mapped to.
+ * @param conversionService the {@link ConversionService} for binding Cassandra values to bean properties, or
+ * {@literal null} for none.
+ * @see #newInstance(Class)
+ * @see #setConversionService
+ */
+ public static BeanPropertyRowMapper newInstance(Class mappedClass,
+ @Nullable ConversionService conversionService) {
+
+ BeanPropertyRowMapper rowMapper = newInstance(mappedClass);
+ rowMapper.setConversionService(conversionService);
+ return rowMapper;
+ }
+
+}
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/DataClassRowMapper.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/DataClassRowMapper.java
new file mode 100644
index 000000000..4454fb6ba
--- /dev/null
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/DataClassRowMapper.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2020 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.cql;
+
+import java.lang.reflect.Constructor;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.TypeConverter;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+import com.datastax.oss.driver.api.core.cql.Row;
+
+/**
+ * {@link RowMapper} implementation that converts a row into a new instance of the specified mapped target class. The
+ * mapped target class must be a top-level class and may either expose a data class constructor with named parameters
+ * corresponding to column names or classic bean property setters (or even a combination of both).
+ *
+ * Note that this class extends {@link BeanPropertyRowMapper} and can therefore serve as a common choice for any mapped
+ * target class, flexibly adapting to constructor style versus setter methods in the mapped class.
+ *
+ * @author Mark Paluch
+ * @since 3.1
+ * @param the result type
+ */
+public class DataClassRowMapper extends BeanPropertyRowMapper {
+
+ private @Nullable Constructor mappedConstructor;
+
+ private @Nullable String[] constructorParameterNames;
+
+ private @Nullable Class>[] constructorParameterTypes;
+
+ /**
+ * Create a new {@code DataClassRowMapper}.
+ *
+ * @param mappedClass the class that each row should be mapped to.
+ */
+ public DataClassRowMapper(Class mappedClass) {
+ super(mappedClass);
+ }
+
+ @Override
+ protected void initialize(Class mappedClass) {
+
+ super.initialize(mappedClass);
+
+ this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass);
+ if (this.mappedConstructor.getParameterCount() > 0) {
+ this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor);
+ this.constructorParameterTypes = this.mappedConstructor.getParameterTypes();
+ }
+ }
+
+ @Override
+ protected T constructMappedInstance(Row row, TypeConverter tc) {
+
+ Assert.state(this.mappedConstructor != null, "Mapped constructor was not initialized");
+
+ Object[] args;
+ if (this.constructorParameterNames != null && this.constructorParameterTypes != null) {
+ args = new Object[this.constructorParameterNames.length];
+ for (int i = 0; i < args.length; i++) {
+ String name = underscoreName(this.constructorParameterNames[i]);
+ Class> type = this.constructorParameterTypes[i];
+ args[i] = tc.convertIfNecessary(getColumnValue(row, row.getColumnDefinitions().firstIndexOf(name), type), type);
+ }
+ } else {
+ args = new Object[0];
+ }
+
+ return BeanUtils.instantiateClass(this.mappedConstructor, args);
+ }
+
+ /**
+ * Static factory method to create a new {@code DataClassRowMapper}.
+ *
+ * @param mappedClass the class that each row should be mapped to.
+ * @see #newInstance(Class, ConversionService)
+ */
+ public static DataClassRowMapper newInstance(Class mappedClass) {
+ return new DataClassRowMapper<>(mappedClass);
+ }
+
+ /**
+ * Static factory method to create a new {@code DataClassRowMapper}.
+ *
+ * @param mappedClass the class that each row should be mapped to.
+ * @param conversionService the {@link ConversionService} for binding Cassandra values to bean properties, or
+ * {@code null} for none.
+ * @see #newInstance(Class)
+ * @see #setConversionService
+ */
+ public static DataClassRowMapper newInstance(Class mappedClass,
+ @Nullable ConversionService conversionService) {
+
+ DataClassRowMapper rowMapper = newInstance(mappedClass);
+ rowMapper.setConversionService(conversionService);
+ return rowMapper;
+ }
+
+}
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/session/init/KeyspacePopulator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/session/init/KeyspacePopulator.java
index 0da07c326..4aa8865af 100644
--- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/session/init/KeyspacePopulator.java
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/session/init/KeyspacePopulator.java
@@ -29,7 +29,7 @@ import com.datastax.oss.driver.api.core.CqlSession;
public interface KeyspacePopulator {
/**
- * Populate, initialize, or clean up the database using the provided JDBC connection.
+ * Populate, initialize, or clean up the database using the provided CqlSession connection.
*
* Concrete implementations may throw a {@link RuntimeException} if an error is encountered but are
* strongly encouraged to throw a specific {@link ScriptException} instead. For example, Spring's
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/session/init/ScriptStatementFailedException.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/session/init/ScriptStatementFailedException.java
index ffa6682a9..197cf6a99 100644
--- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/session/init/ScriptStatementFailedException.java
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/session/init/ScriptStatementFailedException.java
@@ -18,7 +18,7 @@ package org.springframework.data.cassandra.core.cql.session.init;
import org.springframework.core.io.support.EncodedResource;
/**
- * Thrown by {@link ScriptUtils} if a statement in an SQL script failed when executing it against the target database.
+ * Thrown by {@link ScriptUtils} if a statement in an CQL script failed when executing it against the target database.
*
* @author Mark Paluch
* @since 3.0
diff --git a/spring-data-cassandra/src/main/kotlin/org/springframework/data/cassandra/core/cql/DataClassRowMapperExtensions.kt b/spring-data-cassandra/src/main/kotlin/org/springframework/data/cassandra/core/cql/DataClassRowMapperExtensions.kt
new file mode 100644
index 000000000..e69f11c6c
--- /dev/null
+++ b/spring-data-cassandra/src/main/kotlin/org/springframework/data/cassandra/core/cql/DataClassRowMapperExtensions.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020 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.cql
+
+/**
+ * Extensions for [DataClassRowMapper].
+ *
+ * @author Mark Paluch
+ * @since 3.1
+ */
+
+/**
+ * Extension for [DataClassRowMapper] leveraging reified type parameters.
+ */
+inline fun DataClassRowMapper(): DataClassRowMapper =
+ DataClassRowMapper(T::class.java)
diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/BeanPropertyRowMapperUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/BeanPropertyRowMapperUnitTests.java
new file mode 100644
index 000000000..4690619d0
--- /dev/null
+++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/BeanPropertyRowMapperUnitTests.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2020 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.cql;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+
+import com.datastax.oss.driver.api.core.CqlIdentifier;
+import com.datastax.oss.driver.api.core.cql.ColumnDefinition;
+import com.datastax.oss.driver.api.core.cql.ColumnDefinitions;
+import com.datastax.oss.driver.api.core.cql.Row;
+
+/**
+ * Unit tests for {@link BeanPropertyRowMapper}.
+ *
+ * @author Mark Paluch
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class BeanPropertyRowMapperUnitTests {
+
+ @Mock Row row;
+
+ @Test // DATACASS-810
+ void createBeanFromRow() {
+
+ ColumnDefinitions definitions = forColumns("firstname", "age");
+ when(row.getColumnDefinitions()).thenReturn(definitions);
+ when(row.get(0, String.class)).thenReturn("Walter");
+ when(row.get(1, int.class)).thenReturn(42);
+
+ BeanPropertyRowMapper rowMapper = new BeanPropertyRowMapper<>(Person.class);
+
+ Person person = rowMapper.mapRow(row, 0);
+
+ assertThat(person.firstname).isEqualTo("Walter");
+ assertThat(person.age).isEqualTo(42);
+ }
+
+ @Test // DATACASS-810
+ void createBeanFromRowWithNullDefault() {
+
+ ColumnDefinitions definitions = forColumns("firstname", "age");
+ when(row.getColumnDefinitions()).thenReturn(definitions);
+ when(row.get(0, String.class)).thenReturn("Walter");
+
+ BeanPropertyRowMapper rowMapper = new BeanPropertyRowMapper<>(Person.class);
+ rowMapper.setPrimitivesDefaultedForNullValue(true);
+
+ Person person = rowMapper.mapRow(row, 0);
+
+ assertThat(person.firstname).isEqualTo("Walter");
+ assertThat(person.age).isEqualTo(0);
+ }
+
+ @Test // DATACASS-810
+ void shouldRefusePartiallyPopulatedResult() {
+
+ ColumnDefinitions definitions = forColumns("age");
+ when(row.getColumnDefinitions()).thenReturn(definitions);
+ when(row.get(0, int.class)).thenReturn(42);
+
+ BeanPropertyRowMapper rowMapper = new BeanPropertyRowMapper<>(Person.class);
+ rowMapper.setCheckFullyPopulated(true);
+
+ assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> rowMapper.mapRow(row, 0));
+ }
+
+ static class Person {
+
+ String firstname;
+ int age;
+
+ public String getFirstname() {
+ return firstname;
+ }
+
+ public void setFirstname(String firstname) {
+ this.firstname = firstname;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+ }
+
+ private static ColumnDefinitions forColumns(String... columns) {
+
+ ColumnDefinitions definitions = mock(ColumnDefinitions.class);
+
+ int index = 0;
+ for (String column : columns) {
+
+ ColumnDefinition columnDefinition = mock(ColumnDefinition.class);
+ when(columnDefinition.getName()).thenReturn(CqlIdentifier.fromInternal(column));
+
+ when(definitions.get(index++)).thenReturn(columnDefinition);
+ }
+
+ when(definitions.size()).thenReturn(index);
+
+ return definitions;
+ }
+
+}
diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/CqlTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/CqlTemplateIntegrationTests.java
index 55f964fa4..4d79f7404 100644
--- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/CqlTemplateIntegrationTests.java
+++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/CqlTemplateIntegrationTests.java
@@ -33,6 +33,7 @@ import org.springframework.data.util.Version;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
+import com.datastax.oss.driver.api.core.data.UdtValue;
/**
* Integration tests for {@link CqlTemplate}.
@@ -41,6 +42,7 @@ import com.datastax.oss.driver.api.core.cql.SimpleStatement;
* @author Tomasz Lelek
*/
class CqlTemplateIntegrationTests extends AbstractKeyspaceCreatingIntegrationTests {
+
private static final Version CASSANDRA_4 = Version.parse("4.0");
private static final AtomicBoolean initialized = new AtomicBoolean();
private CqlTemplate template;
@@ -50,11 +52,17 @@ class CqlTemplateIntegrationTests extends AbstractKeyspaceCreatingIntegrationTes
void before() {
if (initialized.compareAndSet(false, true)) {
+ session.execute("CREATE TYPE IF NOT EXISTS cql_template_user (testname text, happy boolean);");
+ session
+ .execute("CREATE TABLE IF NOT EXISTS cql_template_tests (id text PRIMARY KEY, thetest cql_template_user);");
session.execute("CREATE TABLE IF NOT EXISTS user (id text PRIMARY KEY, username text);");
}
session.execute("TRUNCATE user;");
+ session.execute("TRUNCATE cql_template_tests;");
session.execute("INSERT INTO user (id, username) VALUES ('WHITE', 'Walter');");
+ session.execute(
+ "INSERT INTO cql_template_tests (id, thetest) VALUES ('shouldApplyBeanPropertyRowMapper', {testname: 'shouldApplyBeanPropertyRowMapper', happy: true});");
template = new CqlTemplate();
template.setSession(getSession());
@@ -197,6 +205,38 @@ class CqlTemplateIntegrationTests extends AbstractKeyspaceCreatingIntegrationTes
assertThatThrownBy(() -> template.queryForObject("SELECT id FROM user;", String.class))
.isInstanceOf(CassandraInvalidQueryException.class)
.hasMessageContaining("Keyspace 'non_existing' does not exist");
+ }
+ @Test // DATACASS-810
+ void shouldApplyBeanPropertyRowMapper() {
+
+ CqlTemplateTest result = template.queryForObject("SELECT * FROM cql_template_tests",
+ new BeanPropertyRowMapper<>(CqlTemplateTest.class));
+
+ assertThat(result.getId()).isEqualTo("shouldApplyBeanPropertyRowMapper");
+ assertThat(result.getThetest().getFormattedContents()).contains("testname:'shouldApplyBeanPropertyRowMapper'")
+ .contains("happy:true");
+ }
+
+ static class CqlTemplateTest {
+
+ String id;
+ UdtValue thetest;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public UdtValue getThetest() {
+ return thetest;
+ }
+
+ public void setThetest(UdtValue thetest) {
+ this.thetest = thetest;
+ }
}
}
diff --git a/spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/core/cql/DataClassRowMapperUnitTests.kt b/spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/core/cql/DataClassRowMapperUnitTests.kt
new file mode 100644
index 000000000..b35266a47
--- /dev/null
+++ b/spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/core/cql/DataClassRowMapperUnitTests.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020 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.cql
+
+import com.datastax.oss.driver.api.core.CqlIdentifier
+import com.datastax.oss.driver.api.core.cql.ColumnDefinition
+import com.datastax.oss.driver.api.core.cql.ColumnDefinitions
+import com.datastax.oss.driver.api.core.cql.Row
+import io.mockk.every
+import io.mockk.mockk
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.mockito.Mockito
+
+/**
+ * Unit tests for [DataClassRowMapper].
+ *
+ * @author Mark Paluch
+ */
+class DataClassRowMapperUnitTests {
+
+ val row = mockk(relaxed = true)
+
+ @Test // DATACASS-810
+ fun createBeanFromRow() {
+
+ val definitions = forColumns("firstname", "age")
+
+ every { row.columnDefinitions } returns definitions
+ every { row.get(0, String::class.java) } returns "Walter"
+ every { row.get(1, Int::class.javaPrimitiveType) } returns 42
+
+ val rowMapper = DataClassRowMapper()
+ val person = rowMapper.mapRow(row, 0)
+
+ assertThat(person.firstname).isEqualTo("Walter")
+ assertThat(person.age).isEqualTo(42)
+ }
+
+ data class Person(val firstname: String, val age: Int)
+
+ private fun forColumns(vararg columns: String): ColumnDefinitions {
+
+ val definitions = Mockito.mock(ColumnDefinitions::class.java)
+ var index = 0
+ for (column in columns) {
+ val columnDefinition = Mockito.mock(ColumnDefinition::class.java)
+ val columnIndex = index++
+ Mockito.`when`(columnDefinition.name).thenReturn(CqlIdentifier.fromInternal(column))
+ Mockito.`when`(definitions[columnIndex]).thenReturn(columnDefinition)
+ Mockito.`when`(definitions.firstIndexOf(column)).thenReturn(columnIndex)
+ }
+ Mockito.`when`(definitions.size()).thenReturn(index)
+
+ return definitions
+ }
+}
diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc
index 66cc8c9ea..4cf175db0 100644
--- a/src/main/asciidoc/new-features.adoc
+++ b/src/main/asciidoc/new-features.adoc
@@ -10,6 +10,7 @@ This chapter summarizes changes and new features for each release.
* Reactive SpEL support in `@Query` query methods.
* Configuration of the keyspace per `Statement` through `CqlTemplate` and `QueryOptions`.
* Revised `CqlOperations` with new `queryForStream(…)` methods returning a `Stream` with transparent pagination.
+* `DataClassRowMapper` to map Cassandra results to data classes via constructor creation/bean properties.
[[new-features.3-0-0]]
== What's new in Spring Data for Apache Cassandra 3.0