DATACASS-810 - Add DataClassRowMapper.
DataClassRowMapper can map Cassandra results onto a class following the data class pattern (using constructor properties) or by setting bean properties.
data class Person(val firstname: String, val age: Int)
cqlTemplate.query("SELECT * FROM person", DataClassRowMapper<Person>())
This commit is contained in:
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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".
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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 <T> the result type
|
||||
*/
|
||||
public class BeanPropertyRowMapper<T> implements RowMapper<T> {
|
||||
|
||||
/** Logger available to subclasses. */
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
/** The class we are mapping to. */
|
||||
private @Nullable Class<T> 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<String, PropertyDescriptor> mappedFields;
|
||||
|
||||
/** Set of bean properties we provide mapping for. */
|
||||
private @Nullable Set<String> 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<T> 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<T> 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<T> 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<T> getMappedClass() {
|
||||
return this.mappedClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether we're strictly validating that all bean properties have been mapped from corresponding database fields.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<T> 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.
|
||||
* <p>
|
||||
* 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<String> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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 <T> BeanPropertyRowMapper<T> newInstance(Class<T> 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 <T> BeanPropertyRowMapper<T> newInstance(Class<T> mappedClass,
|
||||
@Nullable ConversionService conversionService) {
|
||||
|
||||
BeanPropertyRowMapper<T> rowMapper = newInstance(mappedClass);
|
||||
rowMapper.setConversionService(conversionService);
|
||||
return rowMapper;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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).
|
||||
* <p>
|
||||
* 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 <T> the result type
|
||||
*/
|
||||
public class DataClassRowMapper<T> extends BeanPropertyRowMapper<T> {
|
||||
|
||||
private @Nullable Constructor<T> 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<T> mappedClass) {
|
||||
super(mappedClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(Class<T> 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 <T> DataClassRowMapper<T> newInstance(Class<T> 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 <T> DataClassRowMapper<T> newInstance(Class<T> mappedClass,
|
||||
@Nullable ConversionService conversionService) {
|
||||
|
||||
DataClassRowMapper<T> rowMapper = newInstance(mappedClass);
|
||||
rowMapper.setConversionService(conversionService);
|
||||
return rowMapper;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
* <p>
|
||||
* Concrete implementations <em>may</em> throw a {@link RuntimeException} if an error is encountered but are
|
||||
* <em>strongly encouraged</em> to throw a specific {@link ScriptException} instead. For example, Spring's
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <reified T : Any> DataClassRowMapper(): DataClassRowMapper<T> =
|
||||
DataClassRowMapper(T::class.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<Person> 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<Person> 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<Person> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Row>(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<Person>()
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user