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
This commit is contained in:
Juergen Hoeller
2023-08-15 22:42:45 +02:00
parent 2ab1c5b387
commit ae3bc378d6
8 changed files with 232 additions and 385 deletions

View File

@@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Mark Paluch
* @author Mingyuan Wu
* @author Juergen Hoeller
*/
abstract class AbstractDatabaseClientIntegrationTests {
@@ -92,6 +93,25 @@ abstract class AbstractDatabaseClientIntegrationTests {
.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 @@ abstract class AbstractDatabaseClientIntegrationTests {
.verifyComplete();
}
record ParameterRecord(int id, String name, Integer manual) {
}
record ResultRecord(int id) {
}
}

View File

@@ -103,7 +103,7 @@ class DefaultDatabaseClientUnitTests {
DefaultDatabaseClient databaseClient = (DefaultDatabaseClient) databaseClientBuilder.build();
Flux<Object> flux = databaseClient.inConnectionMany(connection -> Flux.empty());
flux.subscribe(new CoreSubscriber<Object>() {
flux.subscribe(new CoreSubscriber<>() {
Subscription subscription;
@@ -136,13 +136,15 @@ class DefaultDatabaseClientUnitTests {
DatabaseClient databaseClient = databaseClientBuilder.namedParameters(false).build();
databaseClient.sql("SELECT * FROM table WHERE key = $1").bindNull(0,
String.class).then().as(StepVerifier::create).verifyComplete();
databaseClient.sql("SELECT * FROM table WHERE key = $1")
.bindNull(0, String.class)
.then().as(StepVerifier::create).verifyComplete();
verify(statement).bind(0, Parameters.in(String.class));
databaseClient.sql("SELECT * FROM table WHERE key = $1").bindNull("$1",
String.class).then().as(StepVerifier::create).verifyComplete();
databaseClient.sql("SELECT * FROM table WHERE key = $1")
.bindNull("$1", String.class)
.then().as(StepVerifier::create).verifyComplete();
verify(statement).bind("$1", Parameters.in(String.class));
}
@@ -153,15 +155,15 @@ class DefaultDatabaseClientUnitTests {
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
DatabaseClient databaseClient = databaseClientBuilder.namedParameters(false).build();
databaseClient.sql("SELECT * FROM table WHERE key = $1").bind(0,
Parameter.empty(String.class)).then().as(
StepVerifier::create).verifyComplete();
databaseClient.sql("SELECT * FROM table WHERE key = $1")
.bind(0, Parameter.empty(String.class))
.then().as(StepVerifier::create).verifyComplete();
verify(statement).bind(0, Parameters.in(String.class));
databaseClient.sql("SELECT * FROM table WHERE key = $1").bind("$1",
Parameter.empty(String.class)).then().as(
StepVerifier::create).verifyComplete();
databaseClient.sql("SELECT * FROM table WHERE key = $1")
.bind("$1", Parameter.empty(String.class))
.then().as(StepVerifier::create).verifyComplete();
verify(statement).bind("$1", Parameters.in(String.class));
}
@@ -171,8 +173,9 @@ class DefaultDatabaseClientUnitTests {
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM table WHERE key = :key").bindNull("key",
String.class).then().as(StepVerifier::create).verifyComplete();
databaseClient.sql("SELECT * FROM table WHERE key = :key")
.bindNull("key", String.class)
.then().as(StepVerifier::create).verifyComplete();
verify(statement).bind(0, Parameters.in(String.class));
}
@@ -185,9 +188,9 @@ class DefaultDatabaseClientUnitTests {
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql(
"SELECT id, name, manual FROM legoset WHERE name IN (:name)").bind(0,
Arrays.asList("unknown", "dunno", "other")).then().as(
StepVerifier::create).verifyComplete();
"SELECT id, name, manual FROM legoset WHERE name IN (:name)")
.bind(0, Arrays.asList("unknown", "dunno", "other"))
.then().as(StepVerifier::create).verifyComplete();
verify(statement).bind(0, "unknown");
verify(statement).bind(1, "dunno");
@@ -207,8 +210,9 @@ class DefaultDatabaseClientUnitTests {
verify(statement).bind(0, Parameters.in("foo"));
databaseClient.sql("SELECT * FROM table WHERE key = $1").bind("$1",
"foo").then().as(StepVerifier::create).verifyComplete();
databaseClient.sql("SELECT * FROM table WHERE key = $1")
.bind("$1", "foo")
.then().as(StepVerifier::create).verifyComplete();
verify(statement).bind("$1", Parameters.in("foo"));
}
@@ -218,8 +222,33 @@ class DefaultDatabaseClientUnitTests {
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM table WHERE key = :key").bind("key",
"foo").then().as(StepVerifier::create).verifyComplete();
databaseClient.sql("SELECT * FROM table WHERE key = :key")
.bind("key", "foo")
.then().as(StepVerifier::create).verifyComplete();
verify(statement).bind(0, Parameters.in("foo"));
}
@Test
void executeShouldBindBeanByIndex() {
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM table WHERE key = :key")
.bindProperties(new ParameterBean("foo"))
.then().as(StepVerifier::create).verifyComplete();
verify(statement).bind(0, Parameters.in("foo"));
}
@Test
void executeShouldBindRecordByIndex() {
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM table WHERE key = :key")
.bindProperties(new ParameterRecord("foo"))
.then().as(StepVerifier::create).verifyComplete();
verify(statement).bind(0, Parameters.in("foo"));
}
@@ -249,15 +278,15 @@ class DefaultDatabaseClientUnitTests {
MockColumnMetadata.builder().name("name").javaType(String.class).build()).build();
MockResult result = MockResult.builder().row(
MockRow.builder().identified(0, Object.class, "Walter").metadata(metadata).build(),
MockRow.builder().identified(0, Object.class, "White").metadata(metadata).build()
MockRow.builder().identified(0, String.class, "Walter").metadata(metadata).build(),
MockRow.builder().identified(0, String.class, "White").metadata(metadata).build()
).build();
mockStatementFor("SELECT * FROM person", result);
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM person").map(row -> row.get(0))
databaseClient.sql("SELECT * FROM person").mapValue(String.class)
.first()
.as(StepVerifier::create)
.expectNext("Walter")
@@ -270,15 +299,15 @@ class DefaultDatabaseClientUnitTests {
MockColumnMetadata.builder().name("name").javaType(String.class).build()).build();
MockResult result = MockResult.builder().row(
MockRow.builder().identified(0, Object.class, "Walter").metadata(metadata).build(),
MockRow.builder().identified(0, Object.class, "White").metadata(metadata).build()
MockRow.builder().identified(0, String.class, "Walter").metadata(metadata).build(),
MockRow.builder().identified(0, String.class, "White").metadata(metadata).build()
).build();
mockStatementFor("SELECT * FROM person", result);
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM person").map(row -> row.get(0))
databaseClient.sql("SELECT * FROM person").mapValue(String.class)
.all()
.as(StepVerifier::create)
.expectNext("Walter")
@@ -292,15 +321,15 @@ class DefaultDatabaseClientUnitTests {
MockColumnMetadata.builder().name("name").javaType(String.class).build()).build();
MockResult result = MockResult.builder().row(
MockRow.builder().identified(0, Object.class, "Walter").metadata(metadata).build(),
MockRow.builder().identified(0, Object.class, "White").metadata(metadata).build()
MockRow.builder().identified(0, String.class, "Walter").metadata(metadata).build(),
MockRow.builder().identified(0, String.class, "White").metadata(metadata).build()
).build();
mockStatementFor("SELECT * FROM person", result);
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM person").map(row -> row.get(0))
databaseClient.sql("SELECT * FROM person").mapValue(String.class)
.one()
.as(StepVerifier::create)
.verifyError(IncorrectResultSizeDataAccessException.class);
@@ -469,4 +498,22 @@ class DefaultDatabaseClientUnitTests {
return resultBuilder.build();
}
static class ParameterBean {
private final String key;
public ParameterBean(String key) {
this.key = key;
}
public String getKey() {
return key;
}
}
record ParameterRecord(String key) {
}
}

View File

@@ -27,7 +27,6 @@ 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;
@@ -37,6 +36,8 @@ import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for R2DBC-based {@link BeanPropertyRowMapper}.
*
* @author Simon Baslé
* @author Juergen Hoeller
* @since 6.1
*/
class R2dbcBeanPropertyRowMapperTests {
@@ -92,20 +93,6 @@ class R2dbcBeanPropertyRowMapperTests {
assertThat(result.email).as("email").isEqualTo("mail@example.org");
}
@Test
void mappingRowMissingAttributeRejected() {
Class<ExtendedPerson> mappedClass = ExtendedPerson.class;
MockRow mockRow = SIMPLE_PERSON_ROW;
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 %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.
@Test
void rowTypeAndMappingTypeMisaligned() {
MockRow mockRow = EXTENDED_PERSON_ROW;
@@ -117,27 +104,6 @@ class R2dbcBeanPropertyRowMapperTests {
+ "'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();
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
mapper.setPrimitivesDefaultedForNullValue(true);
Person result = mapper.apply(mockRow);
assertThat(result.getAge()).isZero();
}
@ParameterizedTest
@CsvSource({
"age, age",

View File

@@ -30,6 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* Test for R2DBC-based {@link DataClassRowMapper}.
*
* @author Simon Baslé
* @author Juergen Hoeller
* @since 6.1
*/
class R2dbcDataClassRowMapperTests {