Support for parameter/result records and beans on DatabaseClient
Includes a revision of BeanProperty/DataClassRowMapper with exclusively constructor-based configuration and without JDBC-inherited legacy settings. Closes gh-27282 Closes gh-26021
This commit is contained in:
@@ -18,11 +18,9 @@ package org.springframework.r2dbc.core;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.r2dbc.spi.OutParameters;
|
||||
@@ -31,20 +29,14 @@ import io.r2dbc.spi.Readable;
|
||||
import io.r2dbc.spi.ReadableMetadata;
|
||||
import io.r2dbc.spi.Row;
|
||||
import io.r2dbc.spi.RowMetadata;
|
||||
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.TypeConverter;
|
||||
import org.springframework.beans.TypeMismatchException;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -68,14 +60,6 @@ import org.springframework.util.StringUtils;
|
||||
* {@code "select fname as first_name from customer"}, where {@code first_name}
|
||||
* can be mapped to a {@code setFirstName(String)} method in the target class.
|
||||
*
|
||||
* <p>For a {@code NULL} value read from the database, an attempt will be made to
|
||||
* call the corresponding setter method with {@code null}, but in the case of
|
||||
* Java primitives this will result in a {@link TypeMismatchException} by default.
|
||||
* To ignore {@code NULL} database values for all primitive properties in the
|
||||
* target class, set the {@code primitivesDefaultedForNullValue} flag to
|
||||
* {@code true}. See {@link #setPrimitivesDefaultedForNullValue(boolean)} for
|
||||
* details.
|
||||
*
|
||||
* <p>If you need to map to a target class which has a <em>data class</em> constructor
|
||||
* — for example, a Java {@code record} or a Kotlin {@code data} class —
|
||||
* use {@link DataClassRowMapper} instead.
|
||||
@@ -85,147 +69,44 @@ import org.springframework.util.StringUtils;
|
||||
* implementation.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Thomas Risberg
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 6.1
|
||||
* @param <T> the result type
|
||||
* @see DataClassRowMapper
|
||||
*/
|
||||
// Note: this class is adapted from the BeanPropertyRowMapper in spring-jdbc
|
||||
public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
|
||||
|
||||
/** Logger available to subclasses. */
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
/** The class we are mapping to. */
|
||||
@Nullable
|
||||
private Class<T> mappedClass;
|
||||
private final Class<T> mappedClass;
|
||||
|
||||
/** Whether we're strictly validating. */
|
||||
private boolean checkFullyPopulated = false;
|
||||
|
||||
/**
|
||||
* Whether {@code NULL} database values should be ignored for primitive
|
||||
* properties in the target class.
|
||||
* @see #setPrimitivesDefaultedForNullValue(boolean)
|
||||
*/
|
||||
private boolean primitivesDefaultedForNullValue = false;
|
||||
|
||||
/** ConversionService for binding R2DBC values to bean properties. */
|
||||
@Nullable
|
||||
private ConversionService conversionService = DefaultConversionService.getSharedInstance();
|
||||
/** ConversionService for binding result values to bean properties. */
|
||||
private final ConversionService conversionService;
|
||||
|
||||
/** Map of the properties we provide mapping for. */
|
||||
@Nullable
|
||||
private Map<String, PropertyDescriptor> mappedProperties;
|
||||
private final Map<String, PropertyDescriptor> mappedProperties;
|
||||
|
||||
/** Set of bean property names we provide mapping for. */
|
||||
@Nullable
|
||||
private Set<String> mappedPropertyNames;
|
||||
|
||||
/**
|
||||
* Create a new {@code BeanPropertyRowMapper}, accepting unpopulated
|
||||
* properties in the target bean.
|
||||
* @param mappedClass the class that each row/outParameters should be mapped to
|
||||
* Create a new {@code BeanPropertyRowMapper}.
|
||||
* @param mappedClass the class that each row should be mapped to
|
||||
*/
|
||||
public BeanPropertyRowMapper(Class<T> mappedClass) {
|
||||
initialize(mappedClass);
|
||||
this(mappedClass, DefaultConversionService.getSharedInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 columns or
|
||||
* out-parameters
|
||||
* @param conversionService a {@link ConversionService} for binding
|
||||
* result values to bean properties
|
||||
*/
|
||||
public BeanPropertyRowMapper(Class<T> mappedClass, boolean checkFullyPopulated) {
|
||||
initialize(mappedClass);
|
||||
this.checkFullyPopulated = checkFullyPopulated;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 columns or out-parameters.
|
||||
* <p>Default is {@code 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 columns or out-parameters.
|
||||
*/
|
||||
public boolean isCheckFullyPopulated() {
|
||||
return this.checkFullyPopulated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether a {@code NULL} database column or out-parameter value should
|
||||
* be ignored when mapping to a corresponding primitive property in the target class.
|
||||
* <p>Default is {@code false}, throwing an exception when nulls are mapped
|
||||
* to Java primitives.
|
||||
* <p>If this flag is set to {@code true} and you use an <em>ignored</em>
|
||||
* primitive property value from the mapped bean to update the database, the
|
||||
* value in the database will be changed from {@code NULL} to the current value
|
||||
* of that primitive property. That value may be the property's initial value
|
||||
* (potentially Java's default value for the respective primitive type), or
|
||||
* it may be some other value set for the property in the default constructor
|
||||
* (or initialization block) or as a side effect of setting some other property
|
||||
* in the mapped bean.
|
||||
*/
|
||||
public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) {
|
||||
this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the {@code primitivesDefaultedForNullValue} flag.
|
||||
* @see #setPrimitivesDefaultedForNullValue(boolean)
|
||||
*/
|
||||
public boolean isPrimitivesDefaultedForNullValue() {
|
||||
return this.primitivesDefaultedForNullValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link ConversionService} for binding R2DBC values to bean properties,
|
||||
* or {@code 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 R2DBC values to bean properties,
|
||||
* or {@code 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) {
|
||||
public BeanPropertyRowMapper(Class<T> mappedClass, ConversionService conversionService) {
|
||||
Assert.notNull(mappedClass, "Mapped Class must not be null");
|
||||
Assert.notNull(conversionService, "ConversionService must not be null");
|
||||
this.mappedClass = mappedClass;
|
||||
this.conversionService = conversionService;
|
||||
this.mappedProperties = new HashMap<>();
|
||||
this.mappedPropertyNames = new HashSet<>();
|
||||
|
||||
for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(mappedClass)) {
|
||||
if (pd.getWriteMethod() != null) {
|
||||
@@ -235,20 +116,18 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
|
||||
if (!lowerCaseName.equals(underscoreName)) {
|
||||
this.mappedProperties.put(underscoreName, pd);
|
||||
}
|
||||
this.mappedPropertyNames.add(pd.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove the specified property from the mapped properties.
|
||||
* @param propertyName the property name (as used by property descriptors)
|
||||
*/
|
||||
protected void suppressProperty(String propertyName) {
|
||||
if (this.mappedProperties != null) {
|
||||
this.mappedProperties.remove(lowerCaseName(propertyName));
|
||||
this.mappedProperties.remove(underscoreName(propertyName));
|
||||
}
|
||||
this.mappedProperties.remove(lowerCaseName(propertyName));
|
||||
this.mappedProperties.remove(underscoreName(propertyName));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -309,52 +188,22 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
|
||||
|
||||
private <R extends Readable> T mapForReadable(R readable, List<? extends ReadableMetadata> readableMetadatas) {
|
||||
BeanWrapperImpl bw = new BeanWrapperImpl();
|
||||
initBeanWrapper(bw);
|
||||
|
||||
bw.setConversionService(this.conversionService);
|
||||
T mappedObject = constructMappedInstance(readable, readableMetadatas, bw);
|
||||
bw.setBeanInstance(mappedObject);
|
||||
|
||||
Set<String> populatedProperties = (isCheckFullyPopulated() ? new HashSet<>() : null);
|
||||
int readableItemCount = readableMetadatas.size();
|
||||
for(int itemIndex = 0; itemIndex < readableItemCount; itemIndex++) {
|
||||
for (int itemIndex = 0; itemIndex < readableItemCount; itemIndex++) {
|
||||
ReadableMetadata itemMetadata = readableMetadatas.get(itemIndex);
|
||||
String itemName = itemMetadata.getName();
|
||||
String property = lowerCaseName(StringUtils.delete(itemName, " "));
|
||||
PropertyDescriptor pd = (this.mappedProperties != null ? this.mappedProperties.get(property) : null);
|
||||
PropertyDescriptor pd = this.mappedProperties.get(property);
|
||||
if (pd != null) {
|
||||
Object value = getItemValue(readable, itemIndex, pd);
|
||||
// Implementation note: the JDBC mapper can log the column mapping details each time row 0 is encountered
|
||||
// but unfortunately this is not possible in R2DBC as row number is not provided. The BiFunction#apply
|
||||
// cannot be stateful as it could be applied to a different row set, e.g. when resubscribing.
|
||||
try {
|
||||
bw.setPropertyValue(pd.getName(), value);
|
||||
}
|
||||
catch (TypeMismatchException ex) {
|
||||
if (value == null && isPrimitivesDefaultedForNullValue()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
String propertyType = ClassUtils.getQualifiedName(pd.getPropertyType());
|
||||
//here too, we miss the rowNumber information
|
||||
logger.debug("""
|
||||
Ignoring intercepted TypeMismatchException for item '%s' \
|
||||
with null value when setting property '%s' of type '%s' on object: %s"
|
||||
""".formatted(itemName, pd.getName(), propertyType, mappedObject), ex);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
if (populatedProperties != null) {
|
||||
populatedProperties.add(pd.getName());
|
||||
}
|
||||
Object value = getItemValue(readable, itemIndex, pd.getPropertyType());
|
||||
bw.setPropertyValue(pd.getName(), value);
|
||||
}
|
||||
}
|
||||
|
||||
if (populatedProperties != null && !populatedProperties.equals(this.mappedPropertyNames)) {
|
||||
throw new InvalidDataAccessApiUsageException("Given readable does not contain all items " +
|
||||
"necessary to populate object of " + this.mappedClass + ": " + this.mappedPropertyNames);
|
||||
}
|
||||
|
||||
return mappedObject;
|
||||
}
|
||||
|
||||
@@ -369,43 +218,9 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
|
||||
* @return a corresponding instance of the mapped class
|
||||
*/
|
||||
protected T constructMappedInstance(Readable readable, List<? extends ReadableMetadata> itemMetadatas, TypeConverter tc) {
|
||||
Assert.state(this.mappedClass != null, "Mapped class was not specified");
|
||||
return BeanUtils.instantiateClass(this.mappedClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the given BeanWrapper to be used for row mapping or outParameters
|
||||
* mapping.
|
||||
* <p>To be called for each Readable.
|
||||
* <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 an R2DBC object value for the specified item index (a column or an
|
||||
* out-parameter).
|
||||
* <p>The default implementation delegates to
|
||||
* {@link #getItemValue(Readable, int, Class)}.
|
||||
* @param readable is the {@code Row} or {@code OutParameters} holding the data
|
||||
* @param itemIndex is the column index or out-parameter index
|
||||
* @param pd the bean property that each result object is expected to match
|
||||
* @return the Object value
|
||||
* @see #getItemValue(Readable, int, Class)
|
||||
*/
|
||||
@Nullable
|
||||
protected Object getItemValue(Readable readable, int itemIndex, PropertyDescriptor pd) {
|
||||
return getItemValue(readable, itemIndex, pd.getPropertyType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an R2DBC object value for the specified item index (a column or
|
||||
* an out-parameter).
|
||||
@@ -430,30 +245,4 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* R2DBC values to bean properties, or {@code 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,9 +27,8 @@ import org.springframework.beans.TypeConverter;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Mapping {@code Function} implementation that converts an R2DBC {@link Readable}
|
||||
@@ -63,17 +62,13 @@ import org.springframework.util.Assert;
|
||||
* @since 6.1
|
||||
* @param <T> the result type
|
||||
*/
|
||||
// Note: this class is adapted from the DataClassRowMapper in spring-jdbc
|
||||
public class DataClassRowMapper<T> extends BeanPropertyRowMapper<T> {
|
||||
|
||||
@Nullable
|
||||
private Constructor<T> mappedConstructor;
|
||||
private final Constructor<T> mappedConstructor;
|
||||
|
||||
@Nullable
|
||||
private String[] constructorParameterNames;
|
||||
private final String[] constructorParameterNames;
|
||||
|
||||
@Nullable
|
||||
private TypeDescriptor[] constructorParameterTypes;
|
||||
private final TypeDescriptor[] constructorParameterTypes;
|
||||
|
||||
|
||||
/**
|
||||
@@ -81,57 +76,47 @@ public class DataClassRowMapper<T> extends BeanPropertyRowMapper<T> {
|
||||
* @param mappedClass the class that each row should be mapped to
|
||||
*/
|
||||
public DataClassRowMapper(Class<T> mappedClass) {
|
||||
super(mappedClass);
|
||||
this(mappedClass, DefaultConversionService.getSharedInstance());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void initialize(Class<T> mappedClass) {
|
||||
super.initialize(mappedClass);
|
||||
public DataClassRowMapper(Class<T> mappedClass, ConversionService conversionService) {
|
||||
super(mappedClass, conversionService);
|
||||
|
||||
this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass);
|
||||
int paramCount = this.mappedConstructor.getParameterCount();
|
||||
if (paramCount > 0) {
|
||||
this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor);
|
||||
for (String name : this.constructorParameterNames) {
|
||||
suppressProperty(name);
|
||||
}
|
||||
this.constructorParameterTypes = new TypeDescriptor[paramCount];
|
||||
for (int i = 0; i < paramCount; i++) {
|
||||
this.constructorParameterTypes[i] = new TypeDescriptor(new MethodParameter(this.mappedConstructor, i));
|
||||
}
|
||||
this.constructorParameterNames = (paramCount > 0 ?
|
||||
BeanUtils.getParameterNames(this.mappedConstructor) : new String[0]);
|
||||
for (String name : this.constructorParameterNames) {
|
||||
suppressProperty(name);
|
||||
}
|
||||
this.constructorParameterTypes = new TypeDescriptor[paramCount];
|
||||
for (int i = 0; i < paramCount; i++) {
|
||||
this.constructorParameterTypes[i] = new TypeDescriptor(new MethodParameter(this.mappedConstructor, i));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected T constructMappedInstance(Readable readable, List<? extends ReadableMetadata> itemMetadatas, 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 = this.constructorParameterNames[i];
|
||||
int index = findIndex(readable, itemMetadatas, lowerCaseName(name));
|
||||
if (index == -1) {
|
||||
index = findIndex(readable, itemMetadatas, underscoreName(name));
|
||||
}
|
||||
if (index == -1) {
|
||||
throw new DataRetrievalFailureException("Unable to map constructor parameter '" + name + "' to a column or out-parameter");
|
||||
}
|
||||
TypeDescriptor td = this.constructorParameterTypes[i];
|
||||
Object value = getItemValue(readable, index, td.getType());
|
||||
args[i] = tc.convertIfNecessary(value, td.getType(), td);
|
||||
Object[] args = new Object[this.constructorParameterNames.length];
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
String name = this.constructorParameterNames[i];
|
||||
int index = findIndex(itemMetadatas, lowerCaseName(name));
|
||||
if (index == -1) {
|
||||
index = findIndex(itemMetadatas, underscoreName(name));
|
||||
}
|
||||
if (index == -1) {
|
||||
throw new DataRetrievalFailureException(
|
||||
"Unable to map constructor parameter '" + name + "' to a column or out-parameter");
|
||||
}
|
||||
TypeDescriptor td = this.constructorParameterTypes[i];
|
||||
Object value = getItemValue(readable, index, td.getType());
|
||||
args[i] = tc.convertIfNecessary(value, td.getType(), td);
|
||||
}
|
||||
else {
|
||||
args = new Object[0];
|
||||
}
|
||||
|
||||
return BeanUtils.instantiateClass(this.mappedConstructor, args);
|
||||
}
|
||||
|
||||
private int findIndex(Readable readable, List<? extends ReadableMetadata> itemMetadatas, String name) {
|
||||
private int findIndex(List<? extends ReadableMetadata> itemMetadatas, String name) {
|
||||
int index = 0;
|
||||
for (ReadableMetadata itemMetadata : itemMetadatas) {
|
||||
// we use equalsIgnoreCase, similar to RowMetadata#contains(String)
|
||||
@@ -143,30 +128,4 @@ public class DataClassRowMapper<T> extends BeanPropertyRowMapper<T> {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* R2DBC 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import org.springframework.util.Assert;
|
||||
* .first();</pre>
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.3
|
||||
*/
|
||||
public interface DatabaseClient extends ConnectionAccessor {
|
||||
@@ -191,6 +192,14 @@ public interface DatabaseClient extends ConnectionAccessor {
|
||||
*/
|
||||
GenericExecuteSpec bindNull(String name, Class<?> type);
|
||||
|
||||
/**
|
||||
* Bind the bean properties or record components from the given
|
||||
* source object, registering each as a named parameter.
|
||||
* @param source the source object (a JavaBean or record)
|
||||
* @since 6.1
|
||||
*/
|
||||
GenericExecuteSpec bindProperties(Object source);
|
||||
|
||||
/**
|
||||
* Add the given filter to the end of the filter chain.
|
||||
* <p>Filter functions are typically used to invoke methods on the Statement
|
||||
@@ -222,7 +231,7 @@ public interface DatabaseClient extends ConnectionAccessor {
|
||||
* Configure a result mapping {@link Function function} and enter the execution stage.
|
||||
* @param mappingFunction a function that maps from {@link Readable} to the result type
|
||||
* @param <R> the result type
|
||||
* @return a {@link FetchSpec} for configuration what to fetch
|
||||
* @return a {@link RowsFetchSpec} for configuration what to fetch
|
||||
* @since 6.0
|
||||
*/
|
||||
<R> RowsFetchSpec<R> map(Function<? super Readable, R> mappingFunction);
|
||||
@@ -232,12 +241,33 @@ public interface DatabaseClient extends ConnectionAccessor {
|
||||
* @param mappingFunction a function that maps from {@link Row} and {@link RowMetadata}
|
||||
* to the result type
|
||||
* @param <R> the result type
|
||||
* @return a {@link FetchSpec} for configuration what to fetch
|
||||
* @return a {@link RowsFetchSpec} for configuration what to fetch
|
||||
*/
|
||||
<R> RowsFetchSpec<R> map(BiFunction<Row, RowMetadata, R> mappingFunction);
|
||||
|
||||
/**
|
||||
* Perform the SQL call and apply {@link BiFunction function} to the {@link Result}.
|
||||
* Configure a mapping for values in the first column and enter the execution stage.
|
||||
* @param mappedClass the target class (a database-supported value class)
|
||||
* @param <R> the result type
|
||||
* @return a {@link RowsFetchSpec} for configuration what to fetch
|
||||
* @since 6.1
|
||||
* @see Readable#get(int, Class)
|
||||
*/
|
||||
<R> RowsFetchSpec<R> mapValue(Class<R> mappedClass);
|
||||
|
||||
/**
|
||||
* Configure a row mapper for the given mapped class and enter the execution stage.
|
||||
* @param mappedClass the target class (a JavaBean or record) with properties to
|
||||
* map to (bean properties or record components)
|
||||
* @param <R> the result type
|
||||
* @return a {@link RowsFetchSpec} for configuration what to fetch
|
||||
* @since 6.1
|
||||
* @see DataClassRowMapper
|
||||
*/
|
||||
<R> RowsFetchSpec<R> mapProperties(Class<R> mappedClass);
|
||||
|
||||
/**
|
||||
* Perform the SQL call and apply {@link BiFunction function} to the {@link Result}.
|
||||
* @param mappingFunction a function that maps from {@link Result} into a result publisher
|
||||
* @param <R> the result type
|
||||
* @return a {@link Flux} that emits mapped elements
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.r2dbc.core;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
@@ -47,6 +48,7 @@ import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.r2dbc.connection.ConnectionFactoryUtils;
|
||||
@@ -54,6 +56,7 @@ import org.springframework.r2dbc.core.binding.BindMarkersFactory;
|
||||
import org.springframework.r2dbc.core.binding.BindTarget;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -64,6 +67,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Mingyuan Wu
|
||||
* @author Bogdan Ilchyshyn
|
||||
* @author Simon Baslé
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.3
|
||||
* @see DatabaseClient#create(ConnectionFactory)
|
||||
*/
|
||||
@@ -310,6 +314,23 @@ final class DefaultDatabaseClient implements DatabaseClient {
|
||||
return new DefaultGenericExecuteSpec(this.byIndex, byName, this.sqlSupplier, this.filterFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultGenericExecuteSpec bindProperties(Object source) {
|
||||
assertNotPreparedOperation();
|
||||
Assert.notNull(source, "Parameter source must not be null");
|
||||
|
||||
Map<String, Parameter> byName = new LinkedHashMap<>(this.byName);
|
||||
for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(source.getClass())) {
|
||||
if (pd.getReadMethod() != null && pd.getReadMethod().getDeclaringClass() != Object.class) {
|
||||
ReflectionUtils.makeAccessible(pd.getReadMethod());
|
||||
Object value = ReflectionUtils.invokeMethod(pd.getReadMethod(), source);
|
||||
byName.put(pd.getName(), (value != null ? Parameters.in(value) : Parameters.in(pd.getPropertyType())));
|
||||
}
|
||||
}
|
||||
|
||||
return new DefaultGenericExecuteSpec(this.byIndex, byName, this.sqlSupplier, this.filterFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultGenericExecuteSpec filter(StatementFilterFunction filter) {
|
||||
Assert.notNull(filter, "StatementFilterFunction must not be null");
|
||||
@@ -329,6 +350,18 @@ final class DefaultDatabaseClient implements DatabaseClient {
|
||||
return execute(this.sqlSupplier, result -> result.map(mappingFunction));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> RowsFetchSpec<R> mapValue(Class<R> mappedClass) {
|
||||
Assert.notNull(mappedClass, "Mapped class must not be null");
|
||||
return execute(this.sqlSupplier, result -> result.map(row -> row.get(0, mappedClass)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> FetchSpec<R> mapProperties(Class<R> mappedClass) {
|
||||
Assert.notNull(mappedClass, "Mapped class must not be null");
|
||||
return execute(this.sqlSupplier, result -> result.map(new DataClassRowMapper<R>(mappedClass)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> Flux<R> flatMap(Function<Result, Publisher<R>> mappingFunction) {
|
||||
Assert.notNull(mappingFunction, "Mapping function must not be null");
|
||||
@@ -392,24 +425,20 @@ final class DefaultDatabaseClient implements DatabaseClient {
|
||||
return statement;
|
||||
};
|
||||
|
||||
return new ResultFunction(sqlSupplier, statementFunction, this.filterFunction, DefaultDatabaseClient.this.executeFunction);
|
||||
return new ResultFunction(sqlSupplier, statementFunction, this.filterFunction,
|
||||
DefaultDatabaseClient.this.executeFunction);
|
||||
}
|
||||
|
||||
private <T> FetchSpec<T> execute(Supplier<String> sqlSupplier, Function<Result, Publisher<T>> resultAdapter) {
|
||||
ResultFunction resultHandler = getResultFunction(sqlSupplier);
|
||||
|
||||
return new DefaultFetchSpec<>(
|
||||
DefaultDatabaseClient.this,
|
||||
resultHandler,
|
||||
connection -> sumRowsUpdated(resultHandler, connection),
|
||||
resultAdapter);
|
||||
return new DefaultFetchSpec<>(DefaultDatabaseClient.this, resultHandler,
|
||||
connection -> sumRowsUpdated(resultHandler, connection), resultAdapter);
|
||||
}
|
||||
|
||||
private <T> Flux<T> flatMap(Supplier<String> sqlSupplier, Function<Result, Publisher<T>> mappingFunction) {
|
||||
ResultFunction resultHandler = getResultFunction(sqlSupplier);
|
||||
ConnectionFunction<Flux<T>> connectionFunction = new DelegateConnectionFunction<>(resultHandler, cx -> resultHandler
|
||||
.apply(cx)
|
||||
.flatMap(mappingFunction));
|
||||
ConnectionFunction<Flux<T>> connectionFunction = new DelegateConnectionFunction<>(resultHandler,
|
||||
cx -> resultHandler.apply(cx).flatMap(mappingFunction));
|
||||
return inConnectionMany(connectionFunction);
|
||||
}
|
||||
|
||||
@@ -448,8 +477,7 @@ final class DefaultDatabaseClient implements DatabaseClient {
|
||||
|
||||
private void assertNotPreparedOperation() {
|
||||
if (this.sqlSupplier instanceof PreparedOperation<?>) {
|
||||
throw new InvalidDataAccessApiUsageException(
|
||||
"Cannot add bindings to a PreparedOperation");
|
||||
throw new InvalidDataAccessApiUsageException("Cannot add bindings to a PreparedOperation");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,8 +525,7 @@ final class DefaultDatabaseClient implements DatabaseClient {
|
||||
return this.target;
|
||||
case "close":
|
||||
// Handle close method: suppress, not valid.
|
||||
return Mono.error(
|
||||
new UnsupportedOperationException("Close is not supported!"));
|
||||
return Mono.error(new UnsupportedOperationException("Close is not supported!"));
|
||||
}
|
||||
|
||||
// Invoke method on target Connection.
|
||||
|
||||
@@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Mingyuan Wu
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
abstract class AbstractDatabaseClientIntegrationTests {
|
||||
|
||||
@@ -92,6 +93,25 @@ abstract class AbstractDatabaseClientIntegrationTests {
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeInsertWithRecords() {
|
||||
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
|
||||
|
||||
databaseClient.sql("INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)")
|
||||
.bindProperties(new ParameterRecord(42055, "SCHAUFELRADBAGGER", null))
|
||||
.fetch().rowsUpdated()
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(1L)
|
||||
.verifyComplete();
|
||||
|
||||
databaseClient.sql("SELECT id FROM legoset")
|
||||
.mapProperties(ResultRecord.class)
|
||||
.first()
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(actual -> assertThat(actual.id()).isEqualTo(42055))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldTranslateDuplicateKeyException() {
|
||||
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
|
||||
@@ -147,4 +167,11 @@ abstract class AbstractDatabaseClientIntegrationTests {
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
|
||||
record ParameterRecord(int id, String name, Integer manual) {
|
||||
}
|
||||
|
||||
record ResultRecord(int id) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class DefaultDatabaseClientUnitTests {
|
||||
DefaultDatabaseClient databaseClient = (DefaultDatabaseClient) databaseClientBuilder.build();
|
||||
Flux<Object> flux = databaseClient.inConnectionMany(connection -> Flux.empty());
|
||||
|
||||
flux.subscribe(new CoreSubscriber<Object>() {
|
||||
flux.subscribe(new CoreSubscriber<>() {
|
||||
|
||||
Subscription subscription;
|
||||
|
||||
@@ -136,13 +136,15 @@ class DefaultDatabaseClientUnitTests {
|
||||
|
||||
DatabaseClient databaseClient = databaseClientBuilder.namedParameters(false).build();
|
||||
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = $1").bindNull(0,
|
||||
String.class).then().as(StepVerifier::create).verifyComplete();
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = $1")
|
||||
.bindNull(0, String.class)
|
||||
.then().as(StepVerifier::create).verifyComplete();
|
||||
|
||||
verify(statement).bind(0, Parameters.in(String.class));
|
||||
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = $1").bindNull("$1",
|
||||
String.class).then().as(StepVerifier::create).verifyComplete();
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = $1")
|
||||
.bindNull("$1", String.class)
|
||||
.then().as(StepVerifier::create).verifyComplete();
|
||||
|
||||
verify(statement).bind("$1", Parameters.in(String.class));
|
||||
}
|
||||
@@ -153,15 +155,15 @@ class DefaultDatabaseClientUnitTests {
|
||||
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
|
||||
DatabaseClient databaseClient = databaseClientBuilder.namedParameters(false).build();
|
||||
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = $1").bind(0,
|
||||
Parameter.empty(String.class)).then().as(
|
||||
StepVerifier::create).verifyComplete();
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = $1")
|
||||
.bind(0, Parameter.empty(String.class))
|
||||
.then().as(StepVerifier::create).verifyComplete();
|
||||
|
||||
verify(statement).bind(0, Parameters.in(String.class));
|
||||
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = $1").bind("$1",
|
||||
Parameter.empty(String.class)).then().as(
|
||||
StepVerifier::create).verifyComplete();
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = $1")
|
||||
.bind("$1", Parameter.empty(String.class))
|
||||
.then().as(StepVerifier::create).verifyComplete();
|
||||
|
||||
verify(statement).bind("$1", Parameters.in(String.class));
|
||||
}
|
||||
@@ -171,8 +173,9 @@ class DefaultDatabaseClientUnitTests {
|
||||
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
|
||||
DatabaseClient databaseClient = databaseClientBuilder.build();
|
||||
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = :key").bindNull("key",
|
||||
String.class).then().as(StepVerifier::create).verifyComplete();
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = :key")
|
||||
.bindNull("key", String.class)
|
||||
.then().as(StepVerifier::create).verifyComplete();
|
||||
|
||||
verify(statement).bind(0, Parameters.in(String.class));
|
||||
}
|
||||
@@ -185,9 +188,9 @@ class DefaultDatabaseClientUnitTests {
|
||||
DatabaseClient databaseClient = databaseClientBuilder.build();
|
||||
|
||||
databaseClient.sql(
|
||||
"SELECT id, name, manual FROM legoset WHERE name IN (:name)").bind(0,
|
||||
Arrays.asList("unknown", "dunno", "other")).then().as(
|
||||
StepVerifier::create).verifyComplete();
|
||||
"SELECT id, name, manual FROM legoset WHERE name IN (:name)")
|
||||
.bind(0, Arrays.asList("unknown", "dunno", "other"))
|
||||
.then().as(StepVerifier::create).verifyComplete();
|
||||
|
||||
verify(statement).bind(0, "unknown");
|
||||
verify(statement).bind(1, "dunno");
|
||||
@@ -207,8 +210,9 @@ class DefaultDatabaseClientUnitTests {
|
||||
|
||||
verify(statement).bind(0, Parameters.in("foo"));
|
||||
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = $1").bind("$1",
|
||||
"foo").then().as(StepVerifier::create).verifyComplete();
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = $1")
|
||||
.bind("$1", "foo")
|
||||
.then().as(StepVerifier::create).verifyComplete();
|
||||
|
||||
verify(statement).bind("$1", Parameters.in("foo"));
|
||||
}
|
||||
@@ -218,8 +222,33 @@ class DefaultDatabaseClientUnitTests {
|
||||
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
|
||||
DatabaseClient databaseClient = databaseClientBuilder.build();
|
||||
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = :key").bind("key",
|
||||
"foo").then().as(StepVerifier::create).verifyComplete();
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = :key")
|
||||
.bind("key", "foo")
|
||||
.then().as(StepVerifier::create).verifyComplete();
|
||||
|
||||
verify(statement).bind(0, Parameters.in("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeShouldBindBeanByIndex() {
|
||||
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
|
||||
DatabaseClient databaseClient = databaseClientBuilder.build();
|
||||
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = :key")
|
||||
.bindProperties(new ParameterBean("foo"))
|
||||
.then().as(StepVerifier::create).verifyComplete();
|
||||
|
||||
verify(statement).bind(0, Parameters.in("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeShouldBindRecordByIndex() {
|
||||
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
|
||||
DatabaseClient databaseClient = databaseClientBuilder.build();
|
||||
|
||||
databaseClient.sql("SELECT * FROM table WHERE key = :key")
|
||||
.bindProperties(new ParameterRecord("foo"))
|
||||
.then().as(StepVerifier::create).verifyComplete();
|
||||
|
||||
verify(statement).bind(0, Parameters.in("foo"));
|
||||
}
|
||||
@@ -249,15 +278,15 @@ class DefaultDatabaseClientUnitTests {
|
||||
MockColumnMetadata.builder().name("name").javaType(String.class).build()).build();
|
||||
|
||||
MockResult result = MockResult.builder().row(
|
||||
MockRow.builder().identified(0, Object.class, "Walter").metadata(metadata).build(),
|
||||
MockRow.builder().identified(0, Object.class, "White").metadata(metadata).build()
|
||||
MockRow.builder().identified(0, String.class, "Walter").metadata(metadata).build(),
|
||||
MockRow.builder().identified(0, String.class, "White").metadata(metadata).build()
|
||||
).build();
|
||||
|
||||
mockStatementFor("SELECT * FROM person", result);
|
||||
|
||||
DatabaseClient databaseClient = databaseClientBuilder.build();
|
||||
|
||||
databaseClient.sql("SELECT * FROM person").map(row -> row.get(0))
|
||||
databaseClient.sql("SELECT * FROM person").mapValue(String.class)
|
||||
.first()
|
||||
.as(StepVerifier::create)
|
||||
.expectNext("Walter")
|
||||
@@ -270,15 +299,15 @@ class DefaultDatabaseClientUnitTests {
|
||||
MockColumnMetadata.builder().name("name").javaType(String.class).build()).build();
|
||||
|
||||
MockResult result = MockResult.builder().row(
|
||||
MockRow.builder().identified(0, Object.class, "Walter").metadata(metadata).build(),
|
||||
MockRow.builder().identified(0, Object.class, "White").metadata(metadata).build()
|
||||
MockRow.builder().identified(0, String.class, "Walter").metadata(metadata).build(),
|
||||
MockRow.builder().identified(0, String.class, "White").metadata(metadata).build()
|
||||
).build();
|
||||
|
||||
mockStatementFor("SELECT * FROM person", result);
|
||||
|
||||
DatabaseClient databaseClient = databaseClientBuilder.build();
|
||||
|
||||
databaseClient.sql("SELECT * FROM person").map(row -> row.get(0))
|
||||
databaseClient.sql("SELECT * FROM person").mapValue(String.class)
|
||||
.all()
|
||||
.as(StepVerifier::create)
|
||||
.expectNext("Walter")
|
||||
@@ -292,15 +321,15 @@ class DefaultDatabaseClientUnitTests {
|
||||
MockColumnMetadata.builder().name("name").javaType(String.class).build()).build();
|
||||
|
||||
MockResult result = MockResult.builder().row(
|
||||
MockRow.builder().identified(0, Object.class, "Walter").metadata(metadata).build(),
|
||||
MockRow.builder().identified(0, Object.class, "White").metadata(metadata).build()
|
||||
MockRow.builder().identified(0, String.class, "Walter").metadata(metadata).build(),
|
||||
MockRow.builder().identified(0, String.class, "White").metadata(metadata).build()
|
||||
).build();
|
||||
|
||||
mockStatementFor("SELECT * FROM person", result);
|
||||
|
||||
DatabaseClient databaseClient = databaseClientBuilder.build();
|
||||
|
||||
databaseClient.sql("SELECT * FROM person").map(row -> row.get(0))
|
||||
databaseClient.sql("SELECT * FROM person").mapValue(String.class)
|
||||
.one()
|
||||
.as(StepVerifier::create)
|
||||
.verifyError(IncorrectResultSizeDataAccessException.class);
|
||||
@@ -469,4 +498,22 @@ class DefaultDatabaseClientUnitTests {
|
||||
return resultBuilder.build();
|
||||
}
|
||||
|
||||
|
||||
static class ParameterBean {
|
||||
|
||||
private final String key;
|
||||
|
||||
public ParameterBean(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
record ParameterRecord(String key) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.TypeMismatchException;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -37,6 +36,8 @@ import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
/**
|
||||
* Tests for R2DBC-based {@link BeanPropertyRowMapper}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.1
|
||||
*/
|
||||
class R2dbcBeanPropertyRowMapperTests {
|
||||
@@ -92,20 +93,6 @@ class R2dbcBeanPropertyRowMapperTests {
|
||||
assertThat(result.email).as("email").isEqualTo("mail@example.org");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mappingRowMissingAttributeRejected() {
|
||||
Class<ExtendedPerson> mappedClass = ExtendedPerson.class;
|
||||
MockRow mockRow = SIMPLE_PERSON_ROW;
|
||||
BeanPropertyRowMapper<ExtendedPerson> mapper = new BeanPropertyRowMapper<>(mappedClass, true);
|
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
|
||||
.isThrownBy(() -> mapper.apply(mockRow))
|
||||
.withMessage("Given readable does not contain all items necessary to populate object of %s"
|
||||
+ ": [firstName, lastName, address, age]", mappedClass);
|
||||
}
|
||||
|
||||
// TODO cannot trigger a mapping of a read-only property, as mappedProperties don't include properties without a setter.
|
||||
|
||||
@Test
|
||||
void rowTypeAndMappingTypeMisaligned() {
|
||||
MockRow mockRow = EXTENDED_PERSON_ROW;
|
||||
@@ -117,27 +104,6 @@ class R2dbcBeanPropertyRowMapperTests {
|
||||
+ "'java.lang.String' for property 'address'; simulating type mismatch for address");
|
||||
}
|
||||
|
||||
@Test
|
||||
void usePrimitiveDefaultWithNullValueFromRow() {
|
||||
MockRow mockRow = MockRow.builder()
|
||||
.metadata(MockRowMetadata.builder()
|
||||
.columnMetadata(MockColumnMetadata.builder().name("firstName").javaType(String.class).build())
|
||||
.columnMetadata(MockColumnMetadata.builder().name("lastName").javaType(String.class).build())
|
||||
.columnMetadata(MockColumnMetadata.builder().name("age").javaType(Integer.class).build())
|
||||
.build())
|
||||
.identified(0, String.class, "John")
|
||||
.identified(1, String.class, "Doe")
|
||||
.identified(2, int.class, null)
|
||||
.identified(3, String.class, "123 Sesame Street")
|
||||
.build();
|
||||
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
|
||||
mapper.setPrimitivesDefaultedForNullValue(true);
|
||||
|
||||
Person result = mapper.apply(mockRow);
|
||||
|
||||
assertThat(result.getAge()).isZero();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"age, age",
|
||||
|
||||
@@ -30,6 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
/**
|
||||
* Test for R2DBC-based {@link DataClassRowMapper}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.1
|
||||
*/
|
||||
class R2dbcDataClassRowMapperTests {
|
||||
|
||||
Reference in New Issue
Block a user