From 7f77ae2483609f33d4bc86722ca747e9866791b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Krokviak?= Date: Wed, 17 Jun 2026 12:24:00 +0200 Subject: [PATCH] Add batch update support to JdbcClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce batchUpdate(List) and batchUpdate(SqlParameterSource...) terminal operations on JdbcClient.StatementSpec, delegating to JdbcOperations and NamedParameterJdbcOperations respectively. Batch updates previously required dropping down to JdbcTemplate or NamedParameterJdbcTemplate. Closes gh-33843 Signed-off-by: Jiří Krokviak --- .../ROOT/pages/data-access/jdbc/core.adoc | 27 +++++++++++-- .../jdbc/core/simple/DefaultJdbcClient.java | 10 +++++ .../jdbc/core/simple/JdbcClient.java | 40 +++++++++++++++++-- .../simple/JdbcClientIntegrationTests.java | 30 ++++++++++++++ 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc index 8dd2442558ae..0b0ce90eaf09 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc @@ -680,6 +680,27 @@ provides `firstName` and `lastName` properties, such as the `Actor` class from a .update(); ---- +For batch updates, provide several batches of parameters – either positional parameter +arrays or named ``SqlParameterSource``s – which are executed as a single JDBC batch: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (?, ?)") + .batchUpdate(List.of( + new Object[] {"Leonor", "Watling"}, + new Object[] {"Christian", "Bale"})); +---- + +Or with named parameters: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)") + .batchUpdate( + new MapSqlParameterSource("firstName", "Leonor").addValue("lastName", "Watling"), + new MapSqlParameterSource("firstName", "Christian").addValue("lastName", "Bale")); +---- + The automatic `Actor` class mapping for parameters as well as the query results above is provided through implicit `SimplePropertySqlParameterSource` and `SimplePropertyRowMapper` strategies which are also available for direct use. They can serve as a common replacement @@ -687,9 +708,9 @@ for `BeanPropertySqlParameterSource` and `BeanPropertyRowMapper`/`DataClassRowMa also with `JdbcTemplate` and `NamedParameterJdbcTemplate` themselves. NOTE: `JdbcClient` is a flexible but simplified facade for JDBC query/update statements. -Advanced capabilities such as batch inserts and stored procedure calls typically require -extra customization: consider Spring's `SimpleJdbcInsert` and `SimpleJdbcCall` classes or -plain direct `JdbcTemplate` usage for any such capabilities not available in `JdbcClient`. +Advanced capabilities such as stored procedure calls typically require extra customization: +consider Spring's `SimpleJdbcInsert` and `SimpleJdbcCall` classes or plain direct +`JdbcTemplate` usage for any such capabilities not available in `JdbcClient`. [[jdbc-SQLExceptionTranslator]] diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java index be3616028ff8..94025dd9b168 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java @@ -296,6 +296,16 @@ public int update(KeyHolder generatedKeyHolder, String... keyColumnNames) { this.classicOps.update(statementCreatorForIndexedParamsWithKeys(keyColumnNames), generatedKeyHolder)); } + @Override + public int[] batchUpdate(List batchArgs) { + return this.classicOps.batchUpdate(this.sql, batchArgs); + } + + @Override + public int[] batchUpdate(SqlParameterSource... batchArgs) { + return this.namedParamOps.batchUpdate(this.sql, batchArgs); + } + private boolean useNamedParams() { boolean hasNamedParams = (this.namedParams.hasValues() || this.namedParamSource != this.namedParams); if (hasNamedParams && !this.indexedParams.isEmpty()) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java index e67dfada1132..78d8f4d26bfc 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java @@ -55,9 +55,11 @@ * *

Delegates to {@link org.springframework.jdbc.core.JdbcTemplate} and * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}. - * For complex JDBC operations — for example, batch inserts and stored - * procedure calls — you may use those lower-level template classes directly, - * or alternatively {@link SimpleJdbcInsert} and {@link SimpleJdbcCall}. + * Batch updates are supported through {@link StatementSpec#batchUpdate(List)} and + * {@link StatementSpec#batchUpdate(SqlParameterSource...)}; for other complex JDBC + * operations — for example, stored procedure calls — you may use those + * lower-level template classes directly, or alternatively {@link SimpleJdbcInsert} + * and {@link SimpleJdbcCall}. * * @author Juergen Hoeller * @author Sam Brannen @@ -344,6 +346,38 @@ interface StatementSpec { * @see java.sql.DatabaseMetaData#supportsGetGeneratedKeys() */ int update(KeyHolder generatedKeyHolder, String... keyColumnNames); + + /** + * Execute the provided SQL statement as a batch update, with several + * batches of positional parameters for "?" placeholder resolution. + *

Each element in the given list provides the positional parameter + * values for one statement execution, with each value bound by its + * JDBC index (that is, element index + 1). + * @param batchArgs the batches of positional parameter values, with each + * {@code Object} array representing one set of "?" placeholder values + * @return an array containing the numbers of rows affected by each + * execution in the batch + * @since 7.1 + * @see java.sql.PreparedStatement#executeBatch() + * @see org.springframework.jdbc.core.JdbcOperations#batchUpdate(String, List) + */ + int[] batchUpdate(List batchArgs); + + /** + * Execute the provided SQL statement as a batch update, with several + * batches of named parameters for ":x" placeholder resolution. + *

Each {@link SqlParameterSource} provides the named parameter values + * for one statement execution. Convenient batch parameter sources can be + * built from maps or parameter objects through + * {@link org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils}. + * @param batchArgs the batches of named parameter values + * @return an array containing the numbers of rows affected by each + * execution in the batch + * @since 7.1 + * @see java.sql.PreparedStatement#executeBatch() + * @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations#batchUpdate(String, SqlParameterSource[]) + */ + int[] batchUpdate(SqlParameterSource... batchArgs); } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java index 743803f960f4..10d5c259cc8d 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java @@ -24,6 +24,8 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassRelativeResourceLoader; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.init.DatabasePopulator; @@ -146,6 +148,34 @@ void updateWithGeneratedKeysAndKeyColumnNamesUsingNamedParameters() { assertUser(expectedId, firstName, lastName); } + @Test + void batchUpdateWithIndexedParameters() { + int[] rowsAffected = this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS) + .batchUpdate(List.of( + new Object[] {"Jane", "Smith"}, + new Object[] {"John", "Doe"})); + + assertThat(rowsAffected).containsExactly(1, 1); + assertNumUsers(3); + assertUser(1, "Jane", "Smith"); + assertUser(2, "John", "Doe"); + } + + @Test + void batchUpdateWithNamedParameters() { + SqlParameterSource[] batchArgs = { + new MapSqlParameterSource().addValue("firstName", "Jane").addValue("lastName", "Smith"), + new MapSqlParameterSource().addValue("firstName", "John").addValue("lastName", "Doe") + }; + + int[] rowsAffected = this.jdbcClient.sql(INSERT_WITH_NAMED_PARAMS).batchUpdate(batchArgs); + + assertThat(rowsAffected).containsExactly(1, 1); + assertNumUsers(3); + assertUser(1, "Jane", "Smith"); + assertUser(2, "John", "Doe"); + } + @Nested // gh-34768 class ReusedNamedParameterTests {