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 5d2616770d2..7e3ebc782bf 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,32 +161,10 @@ final class DefaultJdbcClient implements JdbcClient { @Override public StatementSpec param(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(); - if (index < size) { - this.indexedParams.set(index, value); - } - else { - for (int i = size; i < index; i++) { - this.indexedParams.add(null); - } - this.indexedParams.add(value); - } + addIndexedParam(this.indexedParams, jdbcIndex, value); return this; } - private void validateIndexedParamValue(@Nullable Object value) { - if (value instanceof Iterable) { - throw new IllegalArgumentException("Invalid positional parameter value of type Iterable (" + - value.getClass().getSimpleName() + - "): Parameter expansion is only supported with named parameters."); - } - } - @Override public StatementSpec param(int jdbcIndex, @Nullable Object value, int sqlType) { return param(jdbcIndex, new SqlParameterValue(sqlType, value)); @@ -333,6 +311,32 @@ final class DefaultJdbcClient implements JdbcClient { return pscf.newPreparedStatementCreator(this.indexedParams); } + 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 = indexedParams.size(); + if (index < size) { + indexedParams.set(index, value); + } + else { + for (int i = size; i < index; i++) { + indexedParams.add(null); + } + indexedParams.add(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() + + "): Parameter expansion is only supported with named parameters."); + } + } + private class DefaultBatchSpec implements BatchSpec { @@ -355,12 +359,29 @@ final class DefaultJdbcClient implements JdbcClient { 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) { + this.currentNamedParams.addValue(name, value, sqlType); + return this; + } + @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 8a0b659ef0d..11cb084f31e 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 @@ public interface JdbcClient { */ 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 @@ public interface JdbcClient { */ 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 a31ce0b9c5b..c5fed2eeb85 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 @@ -25,6 +25,7 @@ import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; @@ -33,6 +34,9 @@ import javax.sql.DataSource; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InOrder; import org.springframework.jdbc.Customer; import org.springframework.jdbc.core.SqlParameterValue; @@ -42,6 +46,7 @@ import org.springframework.jdbc.support.KeyHolder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -67,21 +72,21 @@ class JdbcClientIndexedParameterTests { private static final String[] COLUMN_NAMES = new String[] {"id", "forename"}; - private Connection connection = mock(); + private final Connection connection = mock(); - private DataSource dataSource = mock(); + private final DataSource dataSource = mock(); - private PreparedStatement preparedStatement = mock(); + private final PreparedStatement preparedStatement = mock(); - private ResultSet resultSet = mock(); + private final ResultSet resultSet = mock(); - private ResultSetMetaData resultSetMetaData = mock(); + private final ResultSetMetaData resultSetMetaData = mock(); - private DatabaseMetaData databaseMetaData = mock(); + private final DatabaseMetaData databaseMetaData = mock(); - private JdbcClient client = JdbcClient.create(dataSource); + private final JdbcClient client = JdbcClient.create(dataSource); - private List params = new ArrayList<>(); + private final List params = new ArrayList<>(); @BeforeEach @@ -323,6 +328,66 @@ class JdbcClientIndexedParameterTests { verify(connection).close(); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateSingleRow(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1}); + } + else { + 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); + InOrder inOrder = inOrder(connection, preparedStatement); + inOrder.verify(connection).prepareStatement(UPDATE_INDEXED_PARAMETERS); + inOrder.verify(preparedStatement).setObject(1, 1); + inOrder.verify(preparedStatement).setObject(2, 1); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateMultipleRows(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1, 1}); + } + else { + given(preparedStatement.executeUpdate()).willReturn(1); + } + + int[] rowsAffected = client.sql(UPDATE_INDEXED_PARAMETERS).batch() + .param(1, 1).param(2, 1).add() + .param(1, 2).param(2, 2).add() + .update(); + + assertThat(rowsAffected).containsExactly(1, 1); + InOrder inOrder = inOrder(connection, preparedStatement); + inOrder.verify(connection).prepareStatement(UPDATE_INDEXED_PARAMETERS); + inOrder.verify(preparedStatement).setObject(1, 1); + inOrder.verify(preparedStatement).setObject(2, 1); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).setObject(1, 2); + inOrder.verify(preparedStatement).setObject(2, 2); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); + } + @Test void updateWithTypedParameters() throws SQLException { given(preparedStatement.executeUpdate()).willReturn(1); @@ -339,6 +404,80 @@ class JdbcClientIndexedParameterTests { verify(connection).close(); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateWithTypedParameters(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1, 1}); + } + else { + 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() + .param(1, new SqlParameterValue(Types.DECIMAL, 2)) + .param(2, new SqlParameterValue(Types.INTEGER, 2)) + .add() + .update(); + + assertThat(rowsAffected).containsExactly(1, 1); + InOrder inOrder = inOrder(connection, preparedStatement); + inOrder.verify(connection).prepareStatement(UPDATE_INDEXED_PARAMETERS); + inOrder.verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + inOrder.verify(preparedStatement).setObject(2, 1, Types.INTEGER); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).setObject(1, 2, Types.DECIMAL); + inOrder.verify(preparedStatement).setObject(2, 2, Types.INTEGER); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateWithParametersAndSqlType(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1, 1}); + } + else { + given(preparedStatement.executeUpdate()).willReturn(1); + } + + int[] rowsAffected = client.sql(UPDATE_INDEXED_PARAMETERS).batch() + .param(1, 1, Types.DECIMAL) + .param(2, 1, Types.INTEGER) + .add() + .param(1, 2, Types.DECIMAL) + .param(2, 2, Types.INTEGER) + .add() + .update(); + + assertThat(rowsAffected).containsExactly(1, 1); + InOrder inOrder = inOrder(connection, preparedStatement); + inOrder.verify(connection).prepareStatement(UPDATE_INDEXED_PARAMETERS); + inOrder.verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + inOrder.verify(preparedStatement).setObject(2, 1, Types.INTEGER); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).setObject(1, 2, Types.DECIMAL); + inOrder.verify(preparedStatement).setObject(2, 2, Types.INTEGER); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); + } + @Test void updateWithGeneratedKeys() throws SQLException { given(resultSetMetaData.getColumnCount()).willReturn(1); @@ -363,28 +502,49 @@ class JdbcClientIndexedParameterTests { verify(connection).close(); } - @Test - void batchUpdateWithGeneratedKeys() throws SQLException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateWithGeneratedKeys(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1, 1}); + } + else { + given(preparedStatement.executeUpdate()).willReturn(1); + } given(resultSetMetaData.getColumnCount()).willReturn(1); - given(resultSetMetaData.getColumnLabel(1)).willReturn("1"); + given(resultSetMetaData.getColumnLabel(1)).willReturn("id"); given(resultSet.getMetaData()).willReturn(resultSetMetaData); - given(resultSet.next()).willReturn(true, false); - given(resultSet.getObject(1)).willReturn(11); - given(preparedStatement.executeUpdate()).willReturn(1); + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getObject(1)).willReturn(11, 12); given(preparedStatement.getGeneratedKeys()).willReturn(resultSet); given(connection.prepareStatement(INSERT_GENERATE_KEYS, PreparedStatement.RETURN_GENERATED_KEYS)) .willReturn(preparedStatement); KeyHolder generatedKeyHolder = new GeneratedKeyHolder(); - int[] rowsAffected = client.sql(INSERT_GENERATE_KEYS).batch().param("rod").add().update(generatedKeyHolder); + int[] rowsAffected = client.sql(INSERT_GENERATE_KEYS).batch() + .param("rod").add() + .param("johnson").add() + .update(generatedKeyHolder); - assertThat(rowsAffected).isEqualTo(new int[] { 1 }); - assertThat(generatedKeyHolder.getKeyList()).hasSize(1); - assertThat(generatedKeyHolder.getKey()).isEqualTo(11); - verify(preparedStatement).setString(1, "rod"); - verify(resultSet).close(); - verify(preparedStatement).close(); - verify(connection).close(); + assertThat(rowsAffected).containsExactly(1, 1); + assertThat(generatedKeyHolder.getKeyList()).containsExactly(Map.of("id", 11), Map.of("id", 12)); + InOrder inOrder = inOrder(connection, preparedStatement, resultSet); + inOrder.verify(connection).prepareStatement(INSERT_GENERATE_KEYS, PreparedStatement.RETURN_GENERATED_KEYS); + inOrder.verify(preparedStatement).setString(1, "rod"); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + else { + inOrder.verify(resultSet).close(); + } + inOrder.verify(preparedStatement).setString(1, "johnson"); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(resultSet).close(); + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); } @Test @@ -411,28 +571,49 @@ class JdbcClientIndexedParameterTests { verify(connection).close(); } - @Test - void batchUpdateWithGeneratedKeysAndKeyColumnNames() throws SQLException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateWithGeneratedKeysAndKeyColumnNames(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1, 1}); + } + else { + given(preparedStatement.executeUpdate()).willReturn(1); + } given(resultSetMetaData.getColumnCount()).willReturn(1); - given(resultSetMetaData.getColumnLabel(1)).willReturn("1"); + given(resultSetMetaData.getColumnLabel(1)).willReturn("id"); given(resultSet.getMetaData()).willReturn(resultSetMetaData); - given(resultSet.next()).willReturn(true, false); - given(resultSet.getObject(1)).willReturn(11); - given(preparedStatement.executeUpdate()).willReturn(1); + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getObject(1)).willReturn(11, 12); given(preparedStatement.getGeneratedKeys()).willReturn(resultSet); given(connection.prepareStatement(INSERT_GENERATE_KEYS, new String[] {"id"})) .willReturn(preparedStatement); KeyHolder generatedKeyHolder = new GeneratedKeyHolder(); - int[] rowsAffected = client.sql(INSERT_GENERATE_KEYS).batch().param("rod").add().update(generatedKeyHolder, "id"); + int[] rowsAffected = client.sql(INSERT_GENERATE_KEYS).batch() + .param("rod").add() + .param("johnson").add() + .update(generatedKeyHolder, "id"); - assertThat(rowsAffected).isEqualTo(new int[] { 1 }); - assertThat(generatedKeyHolder.getKeyList()).hasSize(1); - assertThat(generatedKeyHolder.getKey()).isEqualTo(11); - verify(preparedStatement).setString(1, "rod"); - verify(resultSet).close(); - verify(preparedStatement).close(); - verify(connection).close(); + assertThat(rowsAffected).containsExactly(1, 1); + assertThat(generatedKeyHolder.getKeyList()).containsExactly(Map.of("id", 11), Map.of("id", 12)); + InOrder inOrder = inOrder(connection, preparedStatement, resultSet); + inOrder.verify(connection).prepareStatement(INSERT_GENERATE_KEYS, new String[] {"id"}); + inOrder.verify(preparedStatement).setString(1, "rod"); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + else { + inOrder.verify(resultSet).close(); + } + inOrder.verify(preparedStatement).setString(1, "johnson"); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(resultSet).close(); + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); } } 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 9906afc3851..83683944a34 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 @@ class JdbcClientIntegrationTests { 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 @@ class JdbcClientIntegrationTests { 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 38b4fae0a09..d39cf58c63e 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 @@ -35,6 +35,9 @@ import javax.sql.DataSource; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InOrder; import org.springframework.jdbc.Customer; import org.springframework.jdbc.core.SqlParameterValue; @@ -45,6 +48,7 @@ import org.springframework.jdbc.support.KeyHolder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -75,23 +79,23 @@ class JdbcClientNamedParameterTests { private static final String[] COLUMN_NAMES = {"id", "forename"}; - private Connection connection = mock(); + private final Connection connection = mock(); - private DataSource dataSource = mock(); + private final DataSource dataSource = mock(); - private PreparedStatement preparedStatement = mock(); + private final PreparedStatement preparedStatement = mock(); - private ResultSet resultSet = mock(); + private final ResultSet resultSet = mock(); - private ResultSetMetaData resultSetMetaData = mock(); + private final ResultSetMetaData resultSetMetaData = mock(); - private DatabaseMetaData databaseMetaData = mock(); + private final DatabaseMetaData databaseMetaData = mock(); - private JdbcClient client = JdbcClient.create(dataSource); + private final JdbcClient client = JdbcClient.create(dataSource); - private Map params = new HashMap<>(); + private final Map params = new HashMap<>(); - private MapSqlParameterSource paramSource = new MapSqlParameterSource(); + private final MapSqlParameterSource paramSource = new MapSqlParameterSource(); @BeforeEach @@ -390,6 +394,66 @@ class JdbcClientNamedParameterTests { verify(connection).close(); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateSingleRow(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1}); + } + else { + 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); + InOrder inOrder = inOrder(connection, preparedStatement); + inOrder.verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + inOrder.verify(preparedStatement).setObject(1, 1); + inOrder.verify(preparedStatement).setObject(2, 1); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateMultipleRows(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1, 1}); + } + else { + given(preparedStatement.executeUpdate()).willReturn(1); + } + + int[] rowsAffected = client.sql(UPDATE_NAMED_PARAMETERS).batch() + .param("perfId", 1).param("priceId", 1).add() + .param("perfId", 2).param("priceId", 2).add() + .update(); + + assertThat(rowsAffected).containsExactly(1, 1); + InOrder inOrder = inOrder(connection, preparedStatement); + inOrder.verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + inOrder.verify(preparedStatement).setObject(1, 1); + inOrder.verify(preparedStatement).setObject(2, 1); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).setObject(1, 2); + inOrder.verify(preparedStatement).setObject(2, 2); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); + } + @Test void updateWithTypedParameters() throws SQLException { given(preparedStatement.executeUpdate()).willReturn(1); @@ -406,6 +470,80 @@ class JdbcClientNamedParameterTests { verify(connection).close(); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateWithTypedParameters(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1, 1}); + } + else { + 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() + .param("perfId", new SqlParameterValue(Types.DECIMAL, 2)) + .param("priceId", new SqlParameterValue(Types.INTEGER, 2)) + .add() + .update(); + + assertThat(rowsAffected).containsExactly(1, 1); + InOrder inOrder = inOrder(connection, preparedStatement); + inOrder.verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + inOrder.verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + inOrder.verify(preparedStatement).setObject(2, 1, Types.INTEGER); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).setObject(1, 2, Types.DECIMAL); + inOrder.verify(preparedStatement).setObject(2, 2, Types.INTEGER); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateWithParametersAndSqlType(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1, 1}); + } + else { + given(preparedStatement.executeUpdate()).willReturn(1); + } + + int[] rowsAffected = client.sql(UPDATE_NAMED_PARAMETERS).batch() + .param("perfId", 1, Types.DECIMAL) + .param("priceId", 1, Types.INTEGER) + .add() + .param("perfId", 2, Types.DECIMAL) + .param("priceId", 2, Types.INTEGER) + .add() + .update(); + + assertThat(rowsAffected).containsExactly(1, 1); + InOrder inOrder = inOrder(connection, preparedStatement); + inOrder.verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + inOrder.verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + inOrder.verify(preparedStatement).setObject(2, 1, Types.INTEGER); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).setObject(1, 2, Types.DECIMAL); + inOrder.verify(preparedStatement).setObject(2, 2, Types.INTEGER); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); + } + @Test void updateWithGeneratedKeys() throws SQLException { given(resultSetMetaData.getColumnCount()).willReturn(1); @@ -430,28 +568,49 @@ class JdbcClientNamedParameterTests { verify(connection).close(); } - @Test - void batchUpdateWithGeneratedKeys() throws SQLException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateWithGeneratedKeys(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1, 1}); + } + else { + given(preparedStatement.executeUpdate()).willReturn(1); + } given(resultSetMetaData.getColumnCount()).willReturn(1); - given(resultSetMetaData.getColumnLabel(1)).willReturn("1"); + given(resultSetMetaData.getColumnLabel(1)).willReturn("id"); given(resultSet.getMetaData()).willReturn(resultSetMetaData); - given(resultSet.next()).willReturn(true, false); - given(resultSet.getObject(1)).willReturn(11); - given(preparedStatement.executeUpdate()).willReturn(1); + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getObject(1)).willReturn(11, 12); given(preparedStatement.getGeneratedKeys()).willReturn(resultSet); given(connection.prepareStatement(INSERT_GENERATE_KEYS_PARSED, PreparedStatement.RETURN_GENERATED_KEYS)) .willReturn(preparedStatement); KeyHolder generatedKeyHolder = new GeneratedKeyHolder(); - int[] rowsAffected = client.sql(INSERT_GENERATE_KEYS).batch().param("name", "rod").add().update(generatedKeyHolder); + int[] rowsAffected = client.sql(INSERT_GENERATE_KEYS).batch() + .param("name", "rod").add() + .param("name", "johnson").add() + .update(generatedKeyHolder); - assertThat(rowsAffected).isEqualTo(new int[] { 1 }); - assertThat(generatedKeyHolder.getKeyList()).hasSize(1); - assertThat(generatedKeyHolder.getKey()).isEqualTo(11); - verify(preparedStatement).setString(1, "rod"); - verify(resultSet).close(); - verify(preparedStatement).close(); - verify(connection).close(); + assertThat(rowsAffected).containsExactly(1, 1); + assertThat(generatedKeyHolder.getKeyList()).containsExactly(Map.of("id", 11), Map.of("id", 12)); + InOrder inOrder = inOrder(connection, preparedStatement, resultSet); + inOrder.verify(connection).prepareStatement(INSERT_GENERATE_KEYS_PARSED, PreparedStatement.RETURN_GENERATED_KEYS); + inOrder.verify(preparedStatement).setString(1, "rod"); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + else { + inOrder.verify(resultSet).close(); + } + inOrder.verify(preparedStatement).setString(1, "johnson"); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(resultSet).close(); + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); } @Test @@ -478,28 +637,49 @@ class JdbcClientNamedParameterTests { verify(connection).close(); } - @Test - void batchUpdateWithGeneratedKeysAndKeyColumnNames() throws SQLException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void batchUpdateWithGeneratedKeysAndKeyColumnNames(boolean supportsBatchUpdates) throws SQLException { + if (supportsBatchUpdates) { + given(connection.getMetaData()).willReturn(databaseMetaData); + given(preparedStatement.executeBatch()).willReturn(new int[] {1, 1}); + } + else { + given(preparedStatement.executeUpdate()).willReturn(1); + } given(resultSetMetaData.getColumnCount()).willReturn(1); - given(resultSetMetaData.getColumnLabel(1)).willReturn("1"); + given(resultSetMetaData.getColumnLabel(1)).willReturn("id"); given(resultSet.getMetaData()).willReturn(resultSetMetaData); - given(resultSet.next()).willReturn(true, false); - given(resultSet.getObject(1)).willReturn(11); - given(preparedStatement.executeUpdate()).willReturn(1); + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getObject(1)).willReturn(11, 12); given(preparedStatement.getGeneratedKeys()).willReturn(resultSet); given(connection.prepareStatement(INSERT_GENERATE_KEYS_PARSED, new String[] {"id"})) .willReturn(preparedStatement); KeyHolder generatedKeyHolder = new GeneratedKeyHolder(); - int[] rowsAffected = client.sql(INSERT_GENERATE_KEYS).batch().param("name", "rod").add().update(generatedKeyHolder, "id"); + int[] rowsAffected = client.sql(INSERT_GENERATE_KEYS).batch() + .param("name", "rod").add() + .param("name", "johnson").add() + .update(generatedKeyHolder, "id"); - assertThat(rowsAffected).isEqualTo(new int[] { 1 }); - assertThat(generatedKeyHolder.getKeyList()).hasSize(1); - assertThat(generatedKeyHolder.getKey()).isEqualTo(11); - verify(preparedStatement).setString(1, "rod"); - verify(resultSet).close(); - verify(preparedStatement).close(); - verify(connection).close(); + assertThat(rowsAffected).containsExactly(1, 1); + assertThat(generatedKeyHolder.getKeyList()).containsExactly(Map.of("id", 11), Map.of("id", 12)); + InOrder inOrder = inOrder(connection, preparedStatement, resultSet); + inOrder.verify(connection).prepareStatement(INSERT_GENERATE_KEYS_PARSED, new String[] {"id"}); + inOrder.verify(preparedStatement).setString(1, "rod"); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + else { + inOrder.verify(resultSet).close(); + } + inOrder.verify(preparedStatement).setString(1, "johnson"); + if (supportsBatchUpdates) { + inOrder.verify(preparedStatement).addBatch(); + } + inOrder.verify(resultSet).close(); + inOrder.verify(preparedStatement).close(); + inOrder.verify(connection).close(); } }