Port BeanPropertyRowMapper and DataClassRowMapper for r2dbc

This commit ports and adapts spring-jdbc's `BeanPropertyRowMapper` and
`DataClassRowMapper` to spring-r2dbc, allowing to `map` rows or
outParameters to object instances, data classes or records.

See gh-26021
Closes gh-30530
This commit is contained in:
Simon Baslé
2023-05-25 17:40:52 +02:00
committed by GitHub
parent 9332b3f690
commit 5977bb05ca
4 changed files with 1091 additions and 0 deletions

View File

@@ -0,0 +1,258 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core;
import io.r2dbc.spi.Readable;
import io.r2dbc.spi.test.MockColumnMetadata;
import io.r2dbc.spi.test.MockOutParameters;
import io.r2dbc.spi.test.MockRow;
import io.r2dbc.spi.test.MockRowMetadata;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
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;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
class BeanPropertyRowMapperTests {
@Test
void mappingUnknownReadableRejected() {
final 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);
assertThatNoException().isThrownBy(() -> mapper.apply(MockOutParameters.empty()));
}
@Test
void mappingRowSimpleObject() {
MockRow mockRow = SIMPLE_PERSON_ROW;
final BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
Person result = mapper.apply(mockRow);
assertThat(result.firstName).as("firstName").isEqualTo("John");
assertThat(result.lastName).as("lastName").isEqualTo("Doe");
assertThat(result.age).as("age").isEqualTo(30);
}
@Test
void mappingRowMissingAttributeAccepted() {
MockRow mockRow = SIMPLE_PERSON_ROW;
final BeanPropertyRowMapper<ExtendedPerson> mapper = new BeanPropertyRowMapper<>(ExtendedPerson.class);
ExtendedPerson result = mapper.apply(mockRow);
assertThat(result.firstName).as("firstName").isEqualTo("John");
assertThat(result.lastName).as("lastName").isEqualTo("Doe");
assertThat(result.age).as("age").isEqualTo(30);
assertThat(result.address).as("address").isNull();
}
@Test
void mappingRowWithDifferentName() {
MockRow mockRow = EMAIL_PERSON_ROW;
final BeanPropertyRowMapper<EmailPerson> mapper = new BeanPropertyRowMapper<>(EmailPerson.class);
EmailPerson result = mapper.apply(mockRow);
assertThat(result.firstName).as("firstName").isEqualTo("John");
assertThat(result.lastName).as("lastName").isEqualTo("Doe");
assertThat(result.age).as("age").isEqualTo(30);
assertThat(result.email).as("email").isEqualTo("mail@example.org");
}
@Test
void mappingRowMissingAttributeRejected() {
MockRow mockRow = SIMPLE_PERSON_ROW;
final BeanPropertyRowMapper<ExtendedPerson> mapper = new BeanPropertyRowMapper<>(ExtendedPerson.class, 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]");
}
//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);
assertThatExceptionOfType(TypeMismatchException.class)
.isThrownBy(() -> mapper.apply(mockRow))
.withMessage("Failed to convert property value of type 'java.lang.String' to required type "
+ "'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();
final BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
mapper.setPrimitivesDefaultedForNullValue(true);
Person result = mapper.apply(mockRow);
assertThat(result.getAge()).isZero();
}
@ParameterizedTest
@CsvSource({
"age, age",
"lastName, last_name",
"Name, name",
"FirstName, first_name",
"EMail, e_mail",
"URL, u_r_l", // likely undesirable, but that's the status quo
})
void underscoreName(String input, String expected) {
BeanPropertyRowMapper<?> mapper = new BeanPropertyRowMapper<>(Object.class);
assertThat(mapper.underscoreName(input)).isEqualTo(expected);
}
private static class Person {
String firstName;
String lastName;
int age;
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
private static class ExtendedPerson extends Person {
String address;
public String getAddress() {
return this.address;
}
public void setAddress(String address) {
this.address = address;
}
}
private static class TypeMismatchExtendedPerson extends ExtendedPerson {
@Override
public void setAddress(String address) {
throw new ClassCastException("simulating type mismatch for address");
}
}
private static class EmailPerson extends Person {
String email;
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
}
private static final MockRow SIMPLE_PERSON_ROW = 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, 30)
.build();
private static final MockRow EXTENDED_PERSON_ROW = 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())
.columnMetadata(MockColumnMetadata.builder().name("address").javaType(String.class).build())
.build())
.identified(0, String.class, "John")
.identified(1, String.class, "Doe")
.identified(2, int.class, 30)
.identified(3, String.class, "123 Sesame Street")
.build();
private static final MockRow EMAIL_PERSON_ROW = buildRowWithExtraColum("EMail", String.class,
String.class, "mail@example.org");
private static final MockRow buildRowWithExtraColum(String extraColumnName, Class<?> extraColumnClass, Class<?> identifiedClass, Object value) {
return MockRow.builder()
.metadata(MockRowMetadata.builder()
.columnMetadata(MockColumnMetadata.builder().name("firstName").javaType(String.class).build())
.columnMetadata(MockColumnMetadata.builder().name("last_name").javaType(String.class).build())
.columnMetadata(MockColumnMetadata.builder().name("age").javaType(Integer.class).build())
.columnMetadata(MockColumnMetadata.builder().name(extraColumnName).javaType(extraColumnClass).build())
.build())
.identified(0, String.class, "John")
.identified(1, String.class, "Doe")
.identified(2, int.class, 30)
.identified(3, identifiedClass, value)
.build();
}
}

View File

@@ -0,0 +1,197 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import io.r2dbc.spi.test.MockColumnMetadata;
import io.r2dbc.spi.test.MockRow;
import io.r2dbc.spi.test.MockRowMetadata;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class DataClassRowMapperTests {
@Test
void staticQueryWithDataClass() {
MockRow mockRow = MOCK_ROW; // uses name, age, birth_date
final DataClassRowMapper<ConstructorPerson> mapper = new DataClassRowMapper<>(ConstructorPerson.class);
ConstructorPerson person = mapper.apply(mockRow);
assertThat(person.name).as("name").isEqualTo("Bubba");
assertThat(person.age).as("age").isEqualTo(22L);
assertThat(person.birth_date).as("birth_date").isNotNull();
}
@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);
ConstructorPersonWithGenerics person = mapper.apply(mockRow);
assertThat(person.name()).isEqualTo("Bubba");
assertThat(person.age()).isEqualTo(22L);
assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L));
assertThat(person.balance()).containsExactly(new BigDecimal("1234.56"));
}
@Test
void staticQueryWithDataRecord() {
MockRow mockRow = MOCK_ROW; // uses name, age, birth_date, balance
final DataClassRowMapper<RecordPerson> mapper = new DataClassRowMapper<>(RecordPerson.class);
RecordPerson person = mapper.apply(mockRow);
assertThat(person.name()).isEqualTo("Bubba");
assertThat(person.age()).isEqualTo(22L);
assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L));
assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56"));
}
@Test
void staticQueryWithDataClassAndSetters() {
MockRow mockRow = buildMockRow("birthdate", false); // uses name, age, birthdate (no underscore), balance
final DataClassRowMapper<ConstructorPersonWithSetters> mapper = new DataClassRowMapper<>(ConstructorPersonWithSetters.class);
ConstructorPersonWithSetters person = mapper.apply(mockRow);
assertThat(person.name()).isEqualTo("BUBBA");
assertThat(person.age()).isEqualTo(22L);
assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L));
assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56"));
}
static class ConstructorPerson {
final String name;
final long age;
final Date birth_date;
public ConstructorPerson(String name, long age, Date birth_date) {
this.name = name;
this.age = age;
this.birth_date = birth_date;
}
public String name() {
return this.name;
}
public long age() {
return this.age;
}
public Date birth_date() {
return this.birth_date;
}
}
static class ConstructorPersonWithGenerics extends ConstructorPerson {
private final List<BigDecimal> balance;
public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List<BigDecimal> balance) {
super(name, age, birth_date);
this.balance = balance;
}
public List<BigDecimal> balance() {
return this.balance;
}
}
static class ConstructorPersonWithSetters {
private String name;
private long age;
private Date birthDate;
private BigDecimal balance;
public ConstructorPersonWithSetters(String name, long age, Date birthDate, BigDecimal balance) {
this.name = name.toUpperCase();
this.age = age;
this.birthDate = birthDate;
this.balance = balance;
}
public void setName(String name) {
this.name = name;
}
public void setAge(long age) {
this.age = age;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public String name() {
return this.name;
}
public long age() {
return this.age;
}
public Date birthDate() {
return this.birthDate;
}
public BigDecimal balance() {
return this.balance;
}
}
static record RecordPerson(String name, long age, Date birth_date, BigDecimal balance) {
}
static MockRow MOCK_ROW = buildMockRow("birth_date", false);
private static MockRow buildMockRow(String birthDateColumnName, boolean balanceObjectIdentifier) {
final 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())
.columnMetadata(MockColumnMetadata.builder().name(birthDateColumnName).javaType(Date.class).build())
.columnMetadata(MockColumnMetadata.builder().name("balance").javaType(BigDecimal.class).build())
.build())
.identified(0, String.class, "Bubba")
.identified(1, long.class, 22)
.identified(2, Date.class, new Date(1221222L))
.identified(3, BigDecimal.class, new BigDecimal("1234.56"));
if (balanceObjectIdentifier) {
builder.identified(3, Object.class, new BigDecimal("1234.56"));
}
return builder.build();
}
}