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 7428bb4af6cd..b0310494439f 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 @@ -161,25 +161,29 @@ public StatementSpec param(@Nullable Object value) { @Override public StatementSpec param(int jdbcIndex, @Nullable Object value) { + addIndexedParam(this.indexedParams, jdbcIndex, value); + return this; + } + + private static void addIndexedParam(List<@Nullable Object> indexedParams, int jdbcIndex, @Nullable Object value) { if (jdbcIndex < 1) { throw new IllegalArgumentException("Invalid JDBC index: needs to start at 1"); } validateIndexedParamValue(value); int index = jdbcIndex - 1; - int size = this.indexedParams.size(); + int size = indexedParams.size(); if (index < size) { - this.indexedParams.set(index, value); + indexedParams.set(index, value); } else { for (int i = size; i < index; i++) { - this.indexedParams.add(null); + indexedParams.add(null); } - this.indexedParams.add(value); + indexedParams.add(value); } - return this; } - private void validateIndexedParamValue(@Nullable Object value) { + private static void validateIndexedParamValue(@Nullable Object value) { if (value instanceof Iterable) { throw new IllegalArgumentException("Invalid positional parameter value of type Iterable (" + value.getClass().getSimpleName() + @@ -355,12 +359,28 @@ public BatchSpec param(@Nullable Object value) { return this; } + @Override + public BatchSpec param(int jdbcIndex, @Nullable Object value) { + addIndexedParam(this.currentIndexedParams, jdbcIndex, value); + return this; + } + + @Override + public BatchSpec param(int jdbcIndex, @Nullable Object value, int sqlType) { + return param(jdbcIndex, new SqlParameterValue(sqlType, value)); + } + @Override public BatchSpec param(String name, @Nullable Object value) { this.currentNamedParams.addValue(name, value); return this; } + @Override + public BatchSpec param(String name, @Nullable Object value, int sqlType) { + return param(name, new SqlParameterValue(sqlType, value)); + } + @Override public BatchSpec params(Object... values) { Collections.addAll(this.currentIndexedParams, values); 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 8a0b659ef0db..11cb084f31e1 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 @@ -398,6 +398,27 @@ interface BatchSpec { */ BatchSpec param(@Nullable Object value); + /** + * Bind a positional JDBC statement parameter for "?" placeholder resolution + * by explicit JDBC statement parameter index. + * @param jdbcIndex the JDBC-style index (starting with 1) + * @param value the parameter value to bind + * @return this batch specification (for chaining) + * @see java.sql.PreparedStatement#setObject(int, Object) + */ + BatchSpec param(int jdbcIndex, @Nullable Object value); + + /** + * Bind a positional JDBC statement parameter for "?" placeholder resolution + * by explicit JDBC statement parameter index. + * @param jdbcIndex the JDBC-style index (starting with 1) + * @param value the parameter value to bind + * @param sqlType the associated SQL type (see {@link java.sql.Types}) + * @return this batch specification (for chaining) + * @see java.sql.PreparedStatement#setObject(int, Object, int) + */ + BatchSpec param(int jdbcIndex, @Nullable Object value, int sqlType); + /** * Bind a named parameter for the current batch entry. * @param name the parameter name @@ -407,6 +428,16 @@ interface BatchSpec { */ BatchSpec param(String name, @Nullable Object value); + /** + * Bind a named parameter for the current batch entry. + * @param name the parameter name + * @param value the parameter value to bind + * @param sqlType the associated SQL type (see {@link java.sql.Types}) + * @return this batch specification (for chaining) + * @see java.sql.PreparedStatement#setObject(int, Object, int) + */ + BatchSpec param(String name, @Nullable Object value, int sqlType); + /** * Bind a var-args list of positional parameters for the current batch entry. * @param values the parameter values to bind diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIndexedParameterTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIndexedParameterTests.java index 8079d72513b2..a7c5a624cd85 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIndexedParameterTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIndexedParameterTests.java @@ -47,6 +47,7 @@ /** * @author Juergen Hoeller + * @author Yanming Zhou * @since 6.1 */ class JdbcClientIndexedParameterTests { @@ -322,6 +323,23 @@ void update() throws SQLException { verify(connection).close(); } + @Test + void batchUpdate() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + int[] rowsAffected = client.sql(UPDATE_INDEXED_PARAMETERS).batch() + .param(1, 1) + .param(2, 1) + .add().update(); + + assertThat(rowsAffected).containsExactly(1); + verify(connection).prepareStatement(UPDATE_INDEXED_PARAMETERS); + verify(preparedStatement).setObject(1, 1); + verify(preparedStatement).setObject(2, 1); + verify(preparedStatement).close(); + verify(connection).close(); + } + @Test void updateWithTypedParameters() throws SQLException { given(preparedStatement.executeUpdate()).willReturn(1); @@ -338,6 +356,40 @@ void updateWithTypedParameters() throws SQLException { verify(connection).close(); } + @Test + void batchUpdateWithTypedParameters() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + int[] rowsAffected = client.sql(UPDATE_INDEXED_PARAMETERS).batch() + .param(1, new SqlParameterValue(Types.DECIMAL, 1)) + .param(2, new SqlParameterValue(Types.INTEGER, 1)) + .add().update(); + + assertThat(rowsAffected).containsExactly(1); + verify(connection).prepareStatement(UPDATE_INDEXED_PARAMETERS); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setObject(2, 1, Types.INTEGER); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + void batchUpdateWithParametersAndSqlType() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + int[] rowsAffected = client.sql(UPDATE_INDEXED_PARAMETERS).batch() + .param(1, 1, Types.DECIMAL) + .param(2, 1, Types.INTEGER) + .add().update(); + + assertThat(rowsAffected).containsExactly(1); + verify(connection).prepareStatement(UPDATE_INDEXED_PARAMETERS); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setObject(2, 1, Types.INTEGER); + verify(preparedStatement).close(); + verify(connection).close(); + } + @Test void updateWithGeneratedKeys() throws SQLException { given(resultSetMetaData.getColumnCount()).willReturn(1); 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 9906afc38518..83683944a346 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 @@ -16,6 +16,7 @@ package org.springframework.jdbc.core.simple; +import java.sql.Types; import java.util.List; import java.util.Map; @@ -165,6 +166,34 @@ void batchUpdateWithIndexedParameters() { assertUser(2, "John", "Doe"); } + @Test + void batchUpdateWithJdbcIndexParameters() { + int[] rowsAffected = this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS) + .batch() + .param(2, "Smith").param(1, "Jane").add() + .param(2, "Doe").param(1, "John") + .update(); + + assertThat(rowsAffected).containsExactly(1, 1); + assertNumUsers(3); + assertUser(1, "Jane", "Smith"); + assertUser(2, "John", "Doe"); + } + + @Test + void batchUpdateWithJdbcIndexParametersAndSqlType() { + int[] rowsAffected = this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS) + .batch() + .param(2, "Smith", Types.VARCHAR).param(1, "Jane", Types.VARCHAR).add() + .param(2, "Doe", Types.VARCHAR).param(1, "John", Types.VARCHAR) + .update(); + + assertThat(rowsAffected).containsExactly(1, 1); + assertNumUsers(3); + assertUser(1, "Jane", "Smith"); + assertUser(2, "John", "Doe"); + } + @Test void batchUpdateWithNamedParameters() { int[] rowsAffected = this.jdbcClient.sql(INSERT_WITH_NAMED_PARAMS) @@ -179,6 +208,20 @@ void batchUpdateWithNamedParameters() { assertUser(2, "John", "Doe"); } + @Test + void batchUpdateWithNamedParametersAndSqlType() { + int[] rowsAffected = this.jdbcClient.sql(INSERT_WITH_NAMED_PARAMS) + .batch() + .param("firstName", "Jane", Types.VARCHAR).param("lastName", "Smith", Types.VARCHAR).add() + .param("firstName", "John", Types.VARCHAR).param("lastName", "Doe", Types.VARCHAR) + .update(); + + assertThat(rowsAffected).containsExactly(1, 1); + assertNumUsers(3); + assertUser(1, "Jane", "Smith"); + assertUser(2, "John", "Doe"); + } + @Test void batchUpdateWithIndividualIndexedParameters() { int[] rowsAffected = this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS) diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientNamedParameterTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientNamedParameterTests.java index 63fe31ad0195..6c1fa299177c 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientNamedParameterTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientNamedParameterTests.java @@ -50,6 +50,7 @@ /** * @author Juergen Hoeller + * @author Yanming Zhou * @since 6.1 */ class JdbcClientNamedParameterTests { @@ -389,6 +390,22 @@ void update() throws SQLException { verify(connection).close(); } + @Test + void batchUpdate() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + int[] rowsAffected = client.sql(UPDATE_NAMED_PARAMETERS).batch() + .param("perfId", 1).param("priceId", 1).add() + .update(); + + assertThat(rowsAffected).containsExactly(1); + verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1); + verify(preparedStatement).setObject(2, 1); + verify(preparedStatement).close(); + verify(connection).close(); + } + @Test void updateWithTypedParameters() throws SQLException { given(preparedStatement.executeUpdate()).willReturn(1); @@ -405,6 +422,42 @@ void updateWithTypedParameters() throws SQLException { verify(connection).close(); } + @Test + void batchUpdateWithTypedParameters() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + int[] rowsAffected = client.sql(UPDATE_NAMED_PARAMETERS).batch() + .param("perfId", new SqlParameterValue(Types.DECIMAL, 1)) + .param("priceId", new SqlParameterValue(Types.INTEGER, 1)) + .add() + .update(); + + assertThat(rowsAffected).containsExactly(1); + verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setObject(2, 1, Types.INTEGER); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + void batchUpdateWithParametersAndSqlType() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + int[] rowsAffected = client.sql(UPDATE_NAMED_PARAMETERS).batch() + .param("perfId", 1, Types.DECIMAL) + .param("priceId", 1, Types.INTEGER) + .add() + .update(); + + assertThat(rowsAffected).containsExactly(1); + verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setObject(2, 1, Types.INTEGER); + verify(preparedStatement).close(); + verify(connection).close(); + } + @Test void updateWithGeneratedKeys() throws SQLException { given(resultSetMetaData.getColumnCount()).willReturn(1);