Polishing

This commit is contained in:
Sam Brannen 2023-06-06 16:03:22 +02:00
parent 66a1be2d86
commit b9e972c248
6 changed files with 103 additions and 94 deletions

View File

@ -274,7 +274,7 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
/**
* Convert the given name to lower case.
* By default, conversions will happen within the US locale.
* <p>By default, conversions will happen within the US locale.
* @param name the original name
* @return the converted name
* @since 4.2
@ -285,7 +285,7 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
/**
* 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.
* <p>Any upper case letters are converted to lower case with a preceding underscore.
* @param name the original name
* @return the converted name
* @since 4.2
@ -390,7 +390,7 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
/**
* Initialize the given BeanWrapper to be used for row mapping.
* To be called for each row.
* <p>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

View File

@ -48,47 +48,46 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Mapping {@code Function} implementation that converts an R2DBC {@code Readable}
* (a {@code Row} or {@code OutParameters}) into a new instance of the specified mapped
* Mapping {@code Function} implementation that converts an R2DBC {@link Readable}
* (a {@link Row} or {@link OutParameters}) into a new instance of the specified mapped
* target class. The mapped target class must be a top-level class or {@code static}
* nested class, and it must have a default or no-arg constructor.
*
* <p>
* Readable component values are mapped based on matching the name (as obtained from R2DBC
* meta-data) to public setters in the target class 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>{@code Readable} component values are mapped based on matching the column
* name (as obtained from R2DBC meta-data) to public setters in the target class
* 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 properties in the target class for many common types &mdash;
* for example: String, boolean, Boolean, byte, Byte, short, Short, int, Integer, long,
* Long, float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc.
* <p>Mapping is provided for properties in the target class for many common types &mdash;
* for example: 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 properties that don't have matching names,
* try using column aliases in the SQL statement like
* {@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>To facilitate mapping between columns and properties that don't have matching
* names, try using column aliases in the SQL statement like
* {@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>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
* &mdash; for example, a Java {@code record} or a Kotlin {@code data} class &mdash; use
* {@link DataClassRowMapper} instead.
* <p>If you need to map to a target class which has a <em>data class</em> constructor
* &mdash; for example, a Java {@code record} or a Kotlin {@code data} class &mdash;
* use {@link DataClassRowMapper} instead.
*
* <p>
* Please note that this class is designed to provide convenience rather than high
* performance. For best performance, consider using a custom mapping function
* <p>Please note that this class is designed to provide convenience rather than
* high performance. For best performance, consider using a custom mapping function
* implementation.
*
* @author Simon Baslé
* @author Thomas Risberg
* @author Juergen Hoeller
* @author Sam Brannen
* @since 6.1
* @param <T> the result type
* @see DataClassRowMapper
@ -254,7 +253,7 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
/**
* Convert the given name to lower case.
* By default, conversions will happen within the US locale.
* <p>By default, conversions will happen within the US locale.
* @param name the original name
* @return the converted name
*/
@ -264,7 +263,7 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
/**
* 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.
* <p>Any upper case letters are converted to lower case with a preceding underscore.
* @param name the original name
* @return the converted name
* @see #lowerCaseName
@ -289,13 +288,11 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
}
/**
* Extract the values for the current {@code Readable} :
* all columns in case of a {@code Row} or all parameters in
* case of an {@code OutParameters}.
* <p>Utilizes public setters and derives meta-data from the
* concrete type.
* @throws UnsupportedOperationException in case the concrete type
* is neither {@code Row} nor {@code OutParameters}
* Extract the values for the current {@link Readable}: all columns in case
* of a {@link Row} or all parameters in case of an {@link OutParameters}.
* <p>Utilizes public setters and derives meta-data from the concrete type.
* @throws IllegalArgumentException in case the concrete type is neither
* {@code Row} nor {@code OutParameters}
* @see RowMetadata
* @see OutParametersMetadata
*/
@ -326,7 +323,7 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
PropertyDescriptor pd = (this.mappedProperties != null ? this.mappedProperties.get(property) : null);
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
// 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 {
@ -363,9 +360,8 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
/**
* Construct an instance of the mapped class for the current {@code Readable}.
* <p>
* The default implementation simply instantiates the mapped class. Can be overridden
* in subclasses.
* <p>The default implementation simply instantiates the mapped class. Can be
* overridden in subclasses.
* @param readable the {@code Readable} being mapped (a {@code Row} or {@code OutParameters})
* @param itemMetadatas the list of item {@code ReadableMetadata} (either
* {@code ColumnMetadata} or {@code OutParameterMetadata})
@ -380,7 +376,7 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
/**
* Initialize the given BeanWrapper to be used for row mapping or outParameters
* mapping.
* To be called for each Readable.
* <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
@ -395,7 +391,7 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
}
/**
* Retrieve a R2DBC object value for the specified item index (a column or an
* 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)}.
@ -411,12 +407,12 @@ public class BeanPropertyRowMapper<T> implements Function<Readable, T> {
}
/**
* Retrieve a R2DBC object value for the specified item index (a column or
* Retrieve an R2DBC object value for the specified item index (a column or
* an out-parameter).
* <p>The default implementation calls {@link Readable#get(int, Class)} then
* falls back to {@link Readable#get(int)} in case of an exception.
* Subclasses may override this to check specific value types upfront,
* or to post-process values return from {@code get}.
* or to post-process values returned from {@code get}.
* @param readable is the {@code Row} or {@code OutParameters} holding the data
* @param itemIndex is the column index or out-parameter index
* @param paramType the target parameter type

View File

@ -32,35 +32,34 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Mapping {@code Function} implementation that converts an R2DBC {@code Readable}
* (a {@code Row} or {@code OutParameters}) into a new instance of the specified mapped
* target class. The mapped target class must be a top-level class or {@code static}
* nested class, and it may expose either a <em>data class</em> constructor with named
* parameters corresponding to column names or classic bean property setter methods
* with property names corresponding to column names (or even a combination of both).
* Mapping {@code Function} implementation that converts an R2DBC {@link Readable}
* (a {@link io.r2dbc.spi.Row Row} or {@link io.r2dbc.spi.OutParameters OutParameters})
* into a new instance of the specified mapped target class. The mapped target class
* must be a top-level class or {@code static} nested class, and it may expose either
* a <em>data class</em> constructor with named parameters corresponding to column
* names or classic bean property setter methods with property names corresponding
* to column names (or even a combination of both).
*
* <p>
* The term "data class" applies to Java <em>records</em>, Kotlin <em>data classes</em>,
* and any class which has a constructor with named parameters that are intended to be
* mapped to corresponding column names.
* <p>The term "data class" applies to Java <em>records</em>, Kotlin <em>data
* classes</em>, and any class which has a constructor with named parameters
* that are intended to be mapped to corresponding column names.
*
* <p>
* When combining a data class constructor with setter methods, any property mapped
* successfully via a constructor argument will not be mapped additionally via a
* corresponding setter method. This means that constructor arguments take precedence over
* property setter methods.
* <p>When combining a data class constructor with setter methods, any property
* mapped successfully via a constructor argument will not be mapped additionally
* via a corresponding setter method. This means that constructor arguments take
* precedence over property setter methods.
*
* <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.
* <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.
*
* <p>
* Please note that this class is designed to provide convenience rather than high
* performance. For best performance, consider using a custom readable mapping
* <p>Please note that this class is designed to provide convenience rather than
* high performance. For best performance, consider using a custom readable mapping
* {@code Function} implementation.
*
* @author Simon Baslé
* @author Juergen Hoeller
* @author Sam Brannen
* @since 6.1
* @param <T> the result type
*/
@ -135,7 +134,7 @@ public class DataClassRowMapper<T> extends BeanPropertyRowMapper<T> {
private int findIndex(Readable readable, List<? extends ReadableMetadata> itemMetadatas, String name) {
int index = 0;
for (ReadableMetadata itemMetadata : itemMetadatas) {
//we use equalsIgnoreCase, similarly to RowMetadata#contains(String)
// we use equalsIgnoreCase, similar to RowMetadata#contains(String)
if (itemMetadata.getName().equalsIgnoreCase(name)) {
return index;
}

View File

@ -34,25 +34,30 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
class BeanPropertyRowMapperTests {
/**
* Tests for R2DBC-based {@link BeanPropertyRowMapper}.
*
* @since 6.1
*/
class R2dbcBeanPropertyRowMapperTests {
@Test
void mappingUnknownReadableRejected() {
final BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
assertThatIllegalArgumentException().isThrownBy(() -> mapper.apply(Mockito.mock(Readable.class)))
.withMessageStartingWith("Can only map Readable Row or OutParameters, got io.r2dbc.spi.Readable$MockitoMock$");
}
@Test
void mappingOutParametersAccepted() {
final BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
assertThatNoException().isThrownBy(() -> mapper.apply(MockOutParameters.empty()));
}
@Test
void mappingRowSimpleObject() {
MockRow mockRow = SIMPLE_PERSON_ROW;
final BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
Person result = mapper.apply(mockRow);
@ -64,7 +69,7 @@ class BeanPropertyRowMapperTests {
@Test
void mappingRowMissingAttributeAccepted() {
MockRow mockRow = SIMPLE_PERSON_ROW;
final BeanPropertyRowMapper<ExtendedPerson> mapper = new BeanPropertyRowMapper<>(ExtendedPerson.class);
BeanPropertyRowMapper<ExtendedPerson> mapper = new BeanPropertyRowMapper<>(ExtendedPerson.class);
ExtendedPerson result = mapper.apply(mockRow);
@ -77,7 +82,7 @@ class BeanPropertyRowMapperTests {
@Test
void mappingRowWithDifferentName() {
MockRow mockRow = EMAIL_PERSON_ROW;
final BeanPropertyRowMapper<EmailPerson> mapper = new BeanPropertyRowMapper<>(EmailPerson.class);
BeanPropertyRowMapper<EmailPerson> mapper = new BeanPropertyRowMapper<>(EmailPerson.class);
EmailPerson result = mapper.apply(mockRow);
@ -89,21 +94,22 @@ class BeanPropertyRowMapperTests {
@Test
void mappingRowMissingAttributeRejected() {
Class<ExtendedPerson> mappedClass = ExtendedPerson.class;
MockRow mockRow = SIMPLE_PERSON_ROW;
final BeanPropertyRowMapper<ExtendedPerson> mapper = new BeanPropertyRowMapper<>(ExtendedPerson.class, true);
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 class org.springframework."
+ "r2dbc.core.BeanPropertyRowMapperTests$ExtendedPerson: [firstName, lastName, address, age]");
.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.
// 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;
final BeanPropertyRowMapper<TypeMismatchExtendedPerson> mapper = new BeanPropertyRowMapper<>(TypeMismatchExtendedPerson.class);
BeanPropertyRowMapper<TypeMismatchExtendedPerson> mapper = new BeanPropertyRowMapper<>(TypeMismatchExtendedPerson.class);
assertThatExceptionOfType(TypeMismatchException.class)
.isThrownBy(() -> mapper.apply(mockRow))
@ -124,7 +130,7 @@ class BeanPropertyRowMapperTests {
.identified(2, int.class, null)
.identified(3, String.class, "123 Sesame Street")
.build();
final BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
mapper.setPrimitivesDefaultedForNullValue(true);
Person result = mapper.apply(mockRow);
@ -147,6 +153,7 @@ class BeanPropertyRowMapperTests {
}
@SuppressWarnings("unused")
private static class Person {
String firstName;
@ -181,6 +188,7 @@ class BeanPropertyRowMapperTests {
}
@SuppressWarnings("unused")
private static class ExtendedPerson extends Person {
String address;
@ -204,6 +212,7 @@ class BeanPropertyRowMapperTests {
}
@SuppressWarnings("unused")
private static class EmailPerson extends Person {
String email;

View File

@ -27,12 +27,17 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class DataClassRowMapperTests {
/**
* Test for R2DBC-based {@link DataClassRowMapper}.
*
* @since 6.1
*/
class R2dbcDataClassRowMapperTests {
@Test
void staticQueryWithDataClass() {
MockRow mockRow = MOCK_ROW; // uses name, age, birth_date
final DataClassRowMapper<ConstructorPerson> mapper = new DataClassRowMapper<>(ConstructorPerson.class);
DataClassRowMapper<ConstructorPerson> mapper = new DataClassRowMapper<>(ConstructorPerson.class);
ConstructorPerson person = mapper.apply(mockRow);
@ -44,8 +49,8 @@ class DataClassRowMapperTests {
@Test
void staticQueryWithDataClassAndGenerics() {
MockRow mockRow = buildMockRow("birth_date", true); // uses name, age, birth_date, balance (as list)
//TODO validate actual R2DBC Row implementations would return something for balance if asking a List
final DataClassRowMapper<ConstructorPersonWithGenerics> mapper = new DataClassRowMapper<>(ConstructorPersonWithGenerics.class);
// TODO validate actual R2DBC Row implementations would return something for balance if requesting a List
DataClassRowMapper<ConstructorPersonWithGenerics> mapper = new DataClassRowMapper<>(ConstructorPersonWithGenerics.class);
ConstructorPersonWithGenerics person = mapper.apply(mockRow);
assertThat(person.name()).isEqualTo("Bubba");
@ -57,7 +62,7 @@ class DataClassRowMapperTests {
@Test
void staticQueryWithDataRecord() {
MockRow mockRow = MOCK_ROW; // uses name, age, birth_date, balance
final DataClassRowMapper<RecordPerson> mapper = new DataClassRowMapper<>(RecordPerson.class);
DataClassRowMapper<RecordPerson> mapper = new DataClassRowMapper<>(RecordPerson.class);
RecordPerson person = mapper.apply(mockRow);
assertThat(person.name()).isEqualTo("Bubba");
@ -69,7 +74,7 @@ class DataClassRowMapperTests {
@Test
void staticQueryWithDataClassAndSetters() {
MockRow mockRow = buildMockRow("birthdate", false); // uses name, age, birthdate (no underscore), balance
final DataClassRowMapper<ConstructorPersonWithSetters> mapper = new DataClassRowMapper<>(ConstructorPersonWithSetters.class);
DataClassRowMapper<ConstructorPersonWithSetters> mapper = new DataClassRowMapper<>(ConstructorPersonWithSetters.class);
ConstructorPersonWithSetters person = mapper.apply(mockRow);
assertThat(person.name()).isEqualTo("BUBBA");
@ -177,10 +182,10 @@ class DataClassRowMapperTests {
}
static MockRow MOCK_ROW = buildMockRow("birth_date", false);
static final MockRow MOCK_ROW = buildMockRow("birth_date", false);
private static MockRow buildMockRow(String birthDateColumnName, boolean balanceObjectIdentifier) {
final MockRow.Builder builder = MockRow.builder();
MockRow.Builder builder = MockRow.builder();
builder.metadata(MockRowMetadata.builder()
.columnMetadata(MockColumnMetadata.builder().name("name").javaType(String.class).build())
.columnMetadata(MockColumnMetadata.builder().name("age").javaType(long.class).build())

View File

@ -766,7 +766,7 @@ class RestTemplateTests {
given(request.getHeaders()).willReturn(requestHeaders);
}
@SuppressWarnings("deprecation")
@SuppressWarnings({ "deprecation", "removal" })
private void mockResponseStatus(HttpStatus responseStatus) throws Exception {
given(request.execute()).willReturn(response);
given(errorHandler.hasError(response)).willReturn(responseStatus.isError());