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:
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user