Skip to content

Commit ae3bc37

Browse files
committed
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
1 parent 2ab1c5b commit ae3bc37

8 files changed

+232
-385
lines changed

Diff for: spring-r2dbc/src/main/java/org/springframework/r2dbc/core/BeanPropertyRowMapper.java

+21-232
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@
1818

1919
import java.beans.PropertyDescriptor;
2020
import java.util.HashMap;
21-
import java.util.HashSet;
2221
import java.util.List;
2322
import java.util.Locale;
2423
import java.util.Map;
25-
import java.util.Set;
2624
import java.util.function.Function;
2725

2826
import io.r2dbc.spi.OutParameters;
@@ -31,20 +29,14 @@
3129
import io.r2dbc.spi.ReadableMetadata;
3230
import io.r2dbc.spi.Row;
3331
import io.r2dbc.spi.RowMetadata;
34-
import org.apache.commons.logging.Log;
35-
import org.apache.commons.logging.LogFactory;
3632

3733
import org.springframework.beans.BeanUtils;
38-
import org.springframework.beans.BeanWrapper;
3934
import org.springframework.beans.BeanWrapperImpl;
4035
import org.springframework.beans.TypeConverter;
41-
import org.springframework.beans.TypeMismatchException;
4236
import org.springframework.core.convert.ConversionService;
4337
import org.springframework.core.convert.support.DefaultConversionService;
44-
import org.springframework.dao.InvalidDataAccessApiUsageException;
4538
import org.springframework.lang.Nullable;
4639
import org.springframework.util.Assert;
47-
import org.springframework.util.ClassUtils;
4840
import org.springframework.util.StringUtils;
4941

5042
/**
@@ -68,14 +60,6 @@
6860
* {@code "select fname as first_name from customer"}, where {@code first_name}
6961
* can be mapped to a {@code setFirstName(String)} method in the target class.
7062
*
71-
* <p>For a {@code NULL} value read from the database, an attempt will be made to
72-
* call the corresponding setter method with {@code null}, but in the case of
73-
* Java primitives this will result in a {@link TypeMismatchException} by default.
74-
* To ignore {@code NULL} database values for all primitive properties in the
75-
* target class, set the {@code primitivesDefaultedForNullValue} flag to
76-
* {@code true}. See {@link #setPrimitivesDefaultedForNullValue(boolean)} for
77-
* details.
78-
*
7963
* <p>If you need to map to a target class which has a <em>data class</em> constructor
8064
* &mdash; for example, a Java {@code record} or a Kotlin {@code data} class &mdash;
8165
* use {@link DataClassRowMapper} instead.
@@ -85,147 +69,44 @@
8569
* implementation.
8670
*
8771
* @author Simon Baslé
88-
* @author Thomas Risberg
8972
* @author Juergen Hoeller
9073
* @author Sam Brannen
9174
* @since 6.1
9275
* @param <T> the result type
9376
* @see DataClassRowMapper
9477
*/
95-
// Note: this class is adapted from the BeanPropertyRowMapper in spring-jdbc
9678
public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
9779

98-
/** Logger available to subclasses. */
99-
protected final Log logger = LogFactory.getLog(getClass());
100-
10180
/** The class we are mapping to. */
102-
@Nullable
103-
private Class<T> mappedClass;
81+
private final Class<T> mappedClass;
10482

105-
/** Whether we're strictly validating. */
106-
private boolean checkFullyPopulated = false;
107-
108-
/**
109-
* Whether {@code NULL} database values should be ignored for primitive
110-
* properties in the target class.
111-
* @see #setPrimitivesDefaultedForNullValue(boolean)
112-
*/
113-
private boolean primitivesDefaultedForNullValue = false;
114-
115-
/** ConversionService for binding R2DBC values to bean properties. */
116-
@Nullable
117-
private ConversionService conversionService = DefaultConversionService.getSharedInstance();
83+
/** ConversionService for binding result values to bean properties. */
84+
private final ConversionService conversionService;
11885

11986
/** Map of the properties we provide mapping for. */
120-
@Nullable
121-
private Map<String, PropertyDescriptor> mappedProperties;
87+
private final Map<String, PropertyDescriptor> mappedProperties;
12288

123-
/** Set of bean property names we provide mapping for. */
124-
@Nullable
125-
private Set<String> mappedPropertyNames;
12689

12790
/**
128-
* Create a new {@code BeanPropertyRowMapper}, accepting unpopulated
129-
* properties in the target bean.
130-
* @param mappedClass the class that each row/outParameters should be mapped to
91+
* Create a new {@code BeanPropertyRowMapper}.
92+
* @param mappedClass the class that each row should be mapped to
13193
*/
13294
public BeanPropertyRowMapper(Class<T> mappedClass) {
133-
initialize(mappedClass);
95+
this(mappedClass, DefaultConversionService.getSharedInstance());
13496
}
13597

13698
/**
13799
* Create a new {@code BeanPropertyRowMapper}.
138100
* @param mappedClass the class that each row should be mapped to
139-
* @param checkFullyPopulated whether we're strictly validating that
140-
* all bean properties have been mapped from corresponding database columns or
141-
* out-parameters
142-
*/
143-
public BeanPropertyRowMapper(Class<T> mappedClass, boolean checkFullyPopulated) {
144-
initialize(mappedClass);
145-
this.checkFullyPopulated = checkFullyPopulated;
146-
}
147-
148-
149-
/**
150-
* Get the class that we are mapping to.
151-
*/
152-
@Nullable
153-
public final Class<T> getMappedClass() {
154-
return this.mappedClass;
155-
}
156-
157-
/**
158-
* Set whether we're strictly validating that all bean properties have been mapped
159-
* from corresponding database columns or out-parameters.
160-
* <p>Default is {@code false}, accepting unpopulated properties in the target bean.
161-
*/
162-
public void setCheckFullyPopulated(boolean checkFullyPopulated) {
163-
this.checkFullyPopulated = checkFullyPopulated;
164-
}
165-
166-
/**
167-
* Return whether we're strictly validating that all bean properties have been
168-
* mapped from corresponding database columns or out-parameters.
101+
* @param conversionService a {@link ConversionService} for binding
102+
* result values to bean properties
169103
*/
170-
public boolean isCheckFullyPopulated() {
171-
return this.checkFullyPopulated;
172-
}
173-
174-
/**
175-
* Set whether a {@code NULL} database column or out-parameter value should
176-
* be ignored when mapping to a corresponding primitive property in the target class.
177-
* <p>Default is {@code false}, throwing an exception when nulls are mapped
178-
* to Java primitives.
179-
* <p>If this flag is set to {@code true} and you use an <em>ignored</em>
180-
* primitive property value from the mapped bean to update the database, the
181-
* value in the database will be changed from {@code NULL} to the current value
182-
* of that primitive property. That value may be the property's initial value
183-
* (potentially Java's default value for the respective primitive type), or
184-
* it may be some other value set for the property in the default constructor
185-
* (or initialization block) or as a side effect of setting some other property
186-
* in the mapped bean.
187-
*/
188-
public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) {
189-
this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue;
190-
}
191-
192-
/**
193-
* Get the value of the {@code primitivesDefaultedForNullValue} flag.
194-
* @see #setPrimitivesDefaultedForNullValue(boolean)
195-
*/
196-
public boolean isPrimitivesDefaultedForNullValue() {
197-
return this.primitivesDefaultedForNullValue;
198-
}
199-
200-
/**
201-
* Set a {@link ConversionService} for binding R2DBC values to bean properties,
202-
* or {@code null} for none.
203-
* <p>Default is a {@link DefaultConversionService}. This provides support for
204-
* {@code java.time} conversion and other special types.
205-
* @see #initBeanWrapper(BeanWrapper)
206-
*/
207-
public void setConversionService(@Nullable ConversionService conversionService) {
208-
this.conversionService = conversionService;
209-
}
210-
211-
/**
212-
* Return a {@link ConversionService} for binding R2DBC values to bean properties,
213-
* or {@code null} if none.
214-
*/
215-
@Nullable
216-
public ConversionService getConversionService() {
217-
return this.conversionService;
218-
}
219-
220-
221-
/**
222-
* Initialize the mapping meta-data for the given class.
223-
* @param mappedClass the mapped class
224-
*/
225-
protected void initialize(Class<T> mappedClass) {
104+
public BeanPropertyRowMapper(Class<T> mappedClass, ConversionService conversionService) {
105+
Assert.notNull(mappedClass, "Mapped Class must not be null");
106+
Assert.notNull(conversionService, "ConversionService must not be null");
226107
this.mappedClass = mappedClass;
108+
this.conversionService = conversionService;
227109
this.mappedProperties = new HashMap<>();
228-
this.mappedPropertyNames = new HashSet<>();
229110

230111
for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(mappedClass)) {
231112
if (pd.getWriteMethod() != null) {
@@ -235,20 +116,18 @@ protected void initialize(Class<T> mappedClass) {
235116
if (!lowerCaseName.equals(underscoreName)) {
236117
this.mappedProperties.put(underscoreName, pd);
237118
}
238-
this.mappedPropertyNames.add(pd.getName());
239119
}
240120
}
241121
}
242122

123+
243124
/**
244125
* Remove the specified property from the mapped properties.
245126
* @param propertyName the property name (as used by property descriptors)
246127
*/
247128
protected void suppressProperty(String propertyName) {
248-
if (this.mappedProperties != null) {
249-
this.mappedProperties.remove(lowerCaseName(propertyName));
250-
this.mappedProperties.remove(underscoreName(propertyName));
251-
}
129+
this.mappedProperties.remove(lowerCaseName(propertyName));
130+
this.mappedProperties.remove(underscoreName(propertyName));
252131
}
253132

254133
/**
@@ -309,52 +188,22 @@ public T apply(Readable readable) {
309188

310189
private <R extends Readable> T mapForReadable(R readable, List<? extends ReadableMetadata> readableMetadatas) {
311190
BeanWrapperImpl bw = new BeanWrapperImpl();
312-
initBeanWrapper(bw);
313-
191+
bw.setConversionService(this.conversionService);
314192
T mappedObject = constructMappedInstance(readable, readableMetadatas, bw);
315193
bw.setBeanInstance(mappedObject);
316194

317-
Set<String> populatedProperties = (isCheckFullyPopulated() ? new HashSet<>() : null);
318195
int readableItemCount = readableMetadatas.size();
319-
for(int itemIndex = 0; itemIndex < readableItemCount; itemIndex++) {
196+
for (int itemIndex = 0; itemIndex < readableItemCount; itemIndex++) {
320197
ReadableMetadata itemMetadata = readableMetadatas.get(itemIndex);
321198
String itemName = itemMetadata.getName();
322199
String property = lowerCaseName(StringUtils.delete(itemName, " "));
323-
PropertyDescriptor pd = (this.mappedProperties != null ? this.mappedProperties.get(property) : null);
200+
PropertyDescriptor pd = this.mappedProperties.get(property);
324201
if (pd != null) {
325-
Object value = getItemValue(readable, itemIndex, pd);
326-
// Implementation note: the JDBC mapper can log the column mapping details each time row 0 is encountered
327-
// but unfortunately this is not possible in R2DBC as row number is not provided. The BiFunction#apply
328-
// cannot be stateful as it could be applied to a different row set, e.g. when resubscribing.
329-
try {
330-
bw.setPropertyValue(pd.getName(), value);
331-
}
332-
catch (TypeMismatchException ex) {
333-
if (value == null && isPrimitivesDefaultedForNullValue()) {
334-
if (logger.isDebugEnabled()) {
335-
String propertyType = ClassUtils.getQualifiedName(pd.getPropertyType());
336-
//here too, we miss the rowNumber information
337-
logger.debug("""
338-
Ignoring intercepted TypeMismatchException for item '%s' \
339-
with null value when setting property '%s' of type '%s' on object: %s"
340-
""".formatted(itemName, pd.getName(), propertyType, mappedObject), ex);
341-
}
342-
}
343-
else {
344-
throw ex;
345-
}
346-
}
347-
if (populatedProperties != null) {
348-
populatedProperties.add(pd.getName());
349-
}
202+
Object value = getItemValue(readable, itemIndex, pd.getPropertyType());
203+
bw.setPropertyValue(pd.getName(), value);
350204
}
351205
}
352206

353-
if (populatedProperties != null && !populatedProperties.equals(this.mappedPropertyNames)) {
354-
throw new InvalidDataAccessApiUsageException("Given readable does not contain all items " +
355-
"necessary to populate object of " + this.mappedClass + ": " + this.mappedPropertyNames);
356-
}
357-
358207
return mappedObject;
359208
}
360209

@@ -369,43 +218,9 @@ private <R extends Readable> T mapForReadable(R readable, List<? extends Readabl
369218
* @return a corresponding instance of the mapped class
370219
*/
371220
protected T constructMappedInstance(Readable readable, List<? extends ReadableMetadata> itemMetadatas, TypeConverter tc) {
372-
Assert.state(this.mappedClass != null, "Mapped class was not specified");
373221
return BeanUtils.instantiateClass(this.mappedClass);
374222
}
375223

376-
/**
377-
* Initialize the given BeanWrapper to be used for row mapping or outParameters
378-
* mapping.
379-
* <p>To be called for each Readable.
380-
* <p>The default implementation applies the configured {@link ConversionService},
381-
* if any. Can be overridden in subclasses.
382-
* @param bw the BeanWrapper to initialize
383-
* @see #getConversionService()
384-
* @see BeanWrapper#setConversionService
385-
*/
386-
protected void initBeanWrapper(BeanWrapper bw) {
387-
ConversionService cs = getConversionService();
388-
if (cs != null) {
389-
bw.setConversionService(cs);
390-
}
391-
}
392-
393-
/**
394-
* Retrieve an R2DBC object value for the specified item index (a column or an
395-
* out-parameter).
396-
* <p>The default implementation delegates to
397-
* {@link #getItemValue(Readable, int, Class)}.
398-
* @param readable is the {@code Row} or {@code OutParameters} holding the data
399-
* @param itemIndex is the column index or out-parameter index
400-
* @param pd the bean property that each result object is expected to match
401-
* @return the Object value
402-
* @see #getItemValue(Readable, int, Class)
403-
*/
404-
@Nullable
405-
protected Object getItemValue(Readable readable, int itemIndex, PropertyDescriptor pd) {
406-
return getItemValue(readable, itemIndex, pd.getPropertyType());
407-
}
408-
409224
/**
410225
* Retrieve an R2DBC object value for the specified item index (a column or
411226
* an out-parameter).
@@ -430,30 +245,4 @@ protected Object getItemValue(Readable readable, int itemIndex, Class<?> paramTy
430245
}
431246
}
432247

433-
434-
/**
435-
* Static factory method to create a new {@code BeanPropertyRowMapper}.
436-
* @param mappedClass the class that each row should be mapped to
437-
* @see #newInstance(Class, ConversionService)
438-
*/
439-
public static <T> BeanPropertyRowMapper<T> newInstance(Class<T> mappedClass) {
440-
return new BeanPropertyRowMapper<>(mappedClass);
441-
}
442-
443-
/**
444-
* Static factory method to create a new {@code BeanPropertyRowMapper}.
445-
* @param mappedClass the class that each row should be mapped to
446-
* @param conversionService the {@link ConversionService} for binding
447-
* R2DBC values to bean properties, or {@code null} for none
448-
* @see #newInstance(Class)
449-
* @see #setConversionService
450-
*/
451-
public static <T> BeanPropertyRowMapper<T> newInstance(
452-
Class<T> mappedClass, @Nullable ConversionService conversionService) {
453-
454-
BeanPropertyRowMapper<T> rowMapper = newInstance(mappedClass);
455-
rowMapper.setConversionService(conversionService);
456-
return rowMapper;
457-
}
458-
459248
}

0 commit comments

Comments
 (0)