Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -680,16 +680,31 @@ provides `firstName` and `lastName` properties, such as the `Actor` class from a
.update();
----

For batch updates, accumulate the batch entries in a fluent fashion through `batch()`,
binding the parameters for each entry as for a single update – either as positional
parameters or as named parameters – and separating consecutive entries with `add()`.
The accumulated entries are executed as a single JDBC batch by `batchUpdate()`, which
also completes the final entry implicitly:

[source,java,indent=0,subs="verbatim,quotes"]
----
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.batch()
.param("firstName", "Leonor").param("lastName", "Watling").add()
.param("firstName", "Christian").param("lastName", "Bale")
.batchUpdate();
----

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
for `BeanPropertySqlParameterSource` and `BeanPropertyRowMapper`/`DataClassRowMapper`,
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]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.springframework.jdbc.core.simple;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand All @@ -30,6 +32,7 @@
import org.springframework.beans.BeanUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
Expand All @@ -56,6 +59,8 @@
*
* @author Juergen Hoeller
* @author Sam Brannen
* @author Jiri Krokviak
* @author Yanming Zhou
* @since 6.1
* @see JdbcClient#create(DataSource)
* @see JdbcClient#create(JdbcOperations)
Expand Down Expand Up @@ -296,6 +301,11 @@ public int update(KeyHolder generatedKeyHolder, String... keyColumnNames) {
this.classicOps.update(statementCreatorForIndexedParamsWithKeys(keyColumnNames), generatedKeyHolder));
}

@Override
public BatchSpec batch() {
return new DefaultBatchSpec();
}

private boolean useNamedParams() {
boolean hasNamedParams = (this.namedParams.hasValues() || this.namedParamSource != this.namedParams);
if (hasNamedParams && !this.indexedParams.isEmpty()) {
Expand Down Expand Up @@ -324,6 +334,162 @@ private PreparedStatementCreator statementCreatorForIndexedParamsWithKeys(String
}


private class DefaultBatchSpec implements BatchSpec {

private final List<Object[]> indexedBatch = new ArrayList<>();

private final List<SqlParameterSource> namedBatch = new ArrayList<>();

private @Nullable Boolean usingNamedParams;

private List<@Nullable Object> currentIndexedParams = new ArrayList<>();

private MapSqlParameterSource currentNamedParams = new MapSqlParameterSource();

private SqlParameterSource currentNamedParamSource = this.currentNamedParams;

@Override
public BatchSpec param(@Nullable Object value) {
validateIndexedParamValue(value);
this.currentIndexedParams.add(value);
return this;
}

@Override
public BatchSpec param(String name, @Nullable Object value) {
this.currentNamedParams.addValue(name, value);
return this;
}

@Override
public BatchSpec params(Object... values) {
Collections.addAll(this.currentIndexedParams, values);
return this;
}

@Override
public BatchSpec params(List<?> values) {
this.currentIndexedParams.addAll(values);
return this;
}

@Override
public BatchSpec params(Map<String, ?> paramMap) {
this.currentNamedParams.addValues(paramMap);
return this;
}

@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public BatchSpec paramSource(Object namedParamObject) {
this.currentNamedParamSource = (namedParamObject instanceof Map map ?
new MapSqlParameterSource(map) :
new SimplePropertySqlParameterSource(namedParamObject));
return this;
}

@Override
public BatchSpec paramSource(SqlParameterSource namedParamSource) {
this.currentNamedParamSource = namedParamSource;
return this;
}

@Override
public BatchSpec add() {
completeEntry();
return this;
}

@Override
public int[] batchUpdate() {
completeEntry();
return (Boolean.TRUE.equals(this.usingNamedParams) ?
namedParamOps.batchUpdate(sql, this.namedBatch.toArray(new SqlParameterSource[0])) :
classicOps.batchUpdate(sql, this.indexedBatch));
}

@Override
public int[] batchUpdate(KeyHolder generatedKeyHolder) {
return doBatchUpdate(generatedKeyHolder, null);
}

@Override
public int[] batchUpdate(KeyHolder generatedKeyHolder, String... keyColumnNames) {
return doBatchUpdate(generatedKeyHolder, keyColumnNames);
}

private int[] doBatchUpdate(KeyHolder generatedKeyHolder, String @Nullable [] keyColumnNames) {
completeEntry();
if (Boolean.TRUE.equals(this.usingNamedParams)) {
if (keyColumnNames != null) {
return namedParamOps.batchUpdate(sql, this.namedBatch.toArray(new SqlParameterSource[0]),
generatedKeyHolder, keyColumnNames);
}
else {
return namedParamOps.batchUpdate(sql, this.namedBatch.toArray(new SqlParameterSource[0]),
generatedKeyHolder);
}
}
else {
if (this.indexedBatch.isEmpty()) {
return new int[0];
}
PreparedStatementCreatorFactory pscf = new PreparedStatementCreatorFactory(sql);
if (keyColumnNames != null) {
pscf.setGeneratedKeysColumnNames(keyColumnNames);
}
else {
pscf.setReturnGeneratedKeys(true);
}
PreparedStatementCreator psc = pscf.newPreparedStatementCreator(this.indexedBatch.get(0));
return classicOps.batchUpdate(psc, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
pscf.newPreparedStatementSetter(indexedBatch.get(i)).setValues(ps);
}

@Override
public int getBatchSize() {
return indexedBatch.size();
}
}, generatedKeyHolder);
}
}

private void completeEntry() {
boolean hasIndexed = !this.currentIndexedParams.isEmpty();
boolean hasNamed = (this.currentNamedParams.hasValues() ||
this.currentNamedParamSource != this.currentNamedParams);
if (hasIndexed && hasNamed) {
throw new IllegalStateException("Configure either named or indexed parameters, not both");
}
if (this.currentNamedParams.hasValues() && this.currentNamedParamSource != this.currentNamedParams) {
throw new IllegalStateException(
"Configure either individual named parameters or a SqlParameterSource, not both");
}
if (!hasIndexed && !hasNamed) {
return;
}
if (this.usingNamedParams == null) {
this.usingNamedParams = hasNamed;
}
else if (this.usingNamedParams != hasNamed) {
throw new IllegalStateException(
"Configure either named or indexed parameters for all batch entries, not both");
}
if (hasNamed) {
this.namedBatch.add(this.currentNamedParamSource);
this.currentNamedParams = new MapSqlParameterSource();
this.currentNamedParamSource = this.currentNamedParams;
}
else {
this.indexedBatch.add(this.currentIndexedParams.toArray());
this.currentIndexedParams = new ArrayList<>();
}
}
}


private class IndexedParamResultQuerySpec implements ResultQuerySpec {

@Override
Expand Down
Loading
Loading