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.
- *
*
If you need to map to a target class which has a data class constructor
* — for example, a Java {@code record} or a Kotlin {@code data} class —
* use {@link DataClassRowMapper} instead.
@@ -85,147 +69,44 @@
* implementation.
*
* @author Simon Baslé
- * @author Thomas Risberg
* @author Juergen Hoeller
* @author Sam Brannen
* @since 6.1
* @param the result type
* @see DataClassRowMapper
*/
-// Note: this class is adapted from the BeanPropertyRowMapper in spring-jdbc
public class BeanPropertyRowMapper implements Function {
- /** Logger available to subclasses. */
- protected final Log logger = LogFactory.getLog(getClass());
-
/** The class we are mapping to. */
- @Nullable
- private Class mappedClass;
+ private final Class 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 mappedProperties;
+ private final Map mappedProperties;
- /** Set of bean property names we provide mapping for. */
- @Nullable
- private Set 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 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
- */
- public BeanPropertyRowMapper(Class mappedClass, boolean checkFullyPopulated) {
- initialize(mappedClass);
- this.checkFullyPopulated = checkFullyPopulated;
- }
-
-
- /**
- * 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 columns or out-parameters.
- *
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.
+ * @param conversionService a {@link ConversionService} for binding
+ * result values to bean properties
*/
- 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.
- *
Default is {@code false}, throwing an exception when nulls are mapped
- * to Java primitives.
- *
If this flag is set to {@code true} and you use an ignored
- * 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.
- *
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 mappedClass) {
+ public BeanPropertyRowMapper(Class 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 @@ protected void initialize(Class mappedClass) {
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 T apply(Readable readable) {
private 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 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 @@ private T mapForReadable(R readable, List extends Readabl
* @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.
- *
To be called for each Readable.
- *
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).
- *
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 @@ protected Object getItemValue(Readable readable, int itemIndex, Class> paramTy
}
}
-
- /**
- * 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
- * R2DBC values to bean properties, or {@code 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-r2dbc/src/main/java/org/springframework/r2dbc/core/DataClassRowMapper.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DataClassRowMapper.java
index 40e53c3c222d..c0b6af51a79c 100644
--- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DataClassRowMapper.java
+++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DataClassRowMapper.java
@@ -27,9 +27,8 @@
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 @@
* @since 6.1
* @param the result type
*/
-// Note: this class is adapted from the DataClassRowMapper in spring-jdbc
public class DataClassRowMapper extends BeanPropertyRowMapper {
- @Nullable
- private Constructor mappedConstructor;
+ private final Constructor mappedConstructor;
- @Nullable
- private String[] constructorParameterNames;
+ private final String[] constructorParameterNames;
- @Nullable
- private TypeDescriptor[] constructorParameterTypes;
+ private final TypeDescriptor[] constructorParameterTypes;
/**
@@ -81,57 +76,47 @@ public class DataClassRowMapper extends BeanPropertyRowMapper {
* @param mappedClass the class that each row should be mapped to
*/
public DataClassRowMapper(Class mappedClass) {
- super(mappedClass);
+ this(mappedClass, DefaultConversionService.getSharedInstance());
}
-
- @Override
- protected void initialize(Class mappedClass) {
- super.initialize(mappedClass);
+ public DataClassRowMapper(Class 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 @@ private int findIndex(Readable readable, List extends ReadableMetadata> itemMe
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 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
- * R2DBC 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-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java
index f0884d07f27f..49e221f6685a 100644
--- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java
+++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java
@@ -55,6 +55,7 @@
* .first();
*
* @author Mark Paluch
+ * @author Juergen Hoeller
* @since 5.3
*/
public interface DatabaseClient extends ConnectionAccessor {
@@ -191,6 +192,14 @@ interface GenericExecuteSpec {
*/
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.
*
Filter functions are typically used to invoke methods on the Statement
@@ -222,7 +231,7 @@ default GenericExecuteSpec filter(Function super Statement, ? extends Statemen
* 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 the result type
- * @return a {@link FetchSpec} for configuration what to fetch
+ * @return a {@link RowsFetchSpec} for configuration what to fetch
* @since 6.0
*/
RowsFetchSpec map(Function super Readable, R> mappingFunction);
@@ -232,12 +241,33 @@ default GenericExecuteSpec filter(Function super Statement, ? extends Statemen
* @param mappingFunction a function that maps from {@link Row} and {@link RowMetadata}
* to the result type
* @param the result type
- * @return a {@link FetchSpec} for configuration what to fetch
+ * @return a {@link RowsFetchSpec} for configuration what to fetch
*/
RowsFetchSpec map(BiFunction 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 the result type
+ * @return a {@link RowsFetchSpec} for configuration what to fetch
+ * @since 6.1
+ * @see Readable#get(int, Class)
+ */
+ RowsFetchSpec mapValue(Class 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 the result type
+ * @return a {@link RowsFetchSpec} for configuration what to fetch
+ * @since 6.1
+ * @see DataClassRowMapper
+ */
+ RowsFetchSpec mapProperties(Class 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 the result type
* @return a {@link Flux} that emits mapped elements
diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java
index 8c75690eb191..56b28c5379c9 100644
--- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java
+++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java
@@ -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 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.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 @@
* @author Mingyuan Wu
* @author Bogdan Ilchyshyn
* @author Simon Baslé
+ * @author Juergen Hoeller
* @since 5.3
* @see DatabaseClient#create(ConnectionFactory)
*/
@@ -310,6 +314,23 @@ public DefaultGenericExecuteSpec bindNull(String name, Class> type) {
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 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 @@ public FetchSpec map(BiFunction mappingFunction) {
return execute(this.sqlSupplier, result -> result.map(mappingFunction));
}
+ @Override
+ public RowsFetchSpec mapValue(Class mappedClass) {
+ Assert.notNull(mappedClass, "Mapped class must not be null");
+ return execute(this.sqlSupplier, result -> result.map(row -> row.get(0, mappedClass)));
+ }
+
+ @Override
+ public FetchSpec mapProperties(Class mappedClass) {
+ Assert.notNull(mappedClass, "Mapped class must not be null");
+ return execute(this.sqlSupplier, result -> result.map(new DataClassRowMapper(mappedClass)));
+ }
+
@Override
public Flux flatMap(Function> mappingFunction) {
Assert.notNull(mappingFunction, "Mapping function must not be null");
@@ -392,24 +425,20 @@ private ResultFunction getResultFunction(Supplier sqlSupplier) {
return statement;
};
- return new ResultFunction(sqlSupplier, statementFunction, this.filterFunction, DefaultDatabaseClient.this.executeFunction);
+ return new ResultFunction(sqlSupplier, statementFunction, this.filterFunction,
+ DefaultDatabaseClient.this.executeFunction);
}
private FetchSpec execute(Supplier sqlSupplier, Function> 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 Flux flatMap(Supplier sqlSupplier, Function> mappingFunction) {
ResultFunction resultHandler = getResultFunction(sqlSupplier);
- ConnectionFunction> connectionFunction = new DelegateConnectionFunction<>(resultHandler, cx -> resultHandler
- .apply(cx)
- .flatMap(mappingFunction));
+ ConnectionFunction> connectionFunction = new DelegateConnectionFunction<>(resultHandler,
+ cx -> resultHandler.apply(cx).flatMap(mappingFunction));
return inConnectionMany(connectionFunction);
}
@@ -448,8 +477,7 @@ private Parameter getParameter(Map remainderByName,
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 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
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.
diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java
index 288afd855b3c..734fef3a506c 100644
--- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java
+++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java
@@ -33,6 +33,7 @@
*
* @author Mark Paluch
* @author Mingyuan Wu
+ * @author Juergen Hoeller
*/
abstract class AbstractDatabaseClientIntegrationTests {
@@ -92,6 +93,25 @@ public void executeInsert() {
.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 @@ public void shouldEmitGeneratedKey() {
.verifyComplete();
}
+
+ record ParameterRecord(int id, String name, Integer manual) {
+ }
+
+ record ResultRecord(int id) {
+ }
+
}
diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/DefaultDatabaseClientUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/DefaultDatabaseClientUnitTests.java
index 8ebf0f1a83ae..5afcb3670c00 100644
--- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/DefaultDatabaseClientUnitTests.java
+++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/DefaultDatabaseClientUnitTests.java
@@ -103,7 +103,7 @@ void shouldCloseConnectionOnlyOnce() {
DefaultDatabaseClient databaseClient = (DefaultDatabaseClient) databaseClientBuilder.build();
Flux