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:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user