JDBC 批量操作
大多数 JDBC 驱动程序在对同一个预处理语句进行多次调用时,如果将这些调用批量处理,可以提供更好的性能。通过将更新分组到批处理中,可以减少与数据库之间的往返次数。
使用 JdbcTemplate
的基本批处理操作
你可以通过实现一个特殊接口 BatchPreparedStatementSetter
的两个方法来完成 JdbcTemplate
的批处理,并在调用 batchUpdate
方法时将该实现作为第二个参数传入。你可以使用 getBatchSize
方法来提供当前批次的大小。你可以使用 setValues
方法来为预准备语句的参数设置值。这个方法会被调用你在 getBatchSize
中指定的次数。下面的示例根据列表中的条目更新 t_actor
表,并将整个列表用作批处理:
public class ActorBatchPreparedStatementSetter implements BatchPreparedStatementSetter {
private List<Actor> actors;
public ActorBatchPreparedStatementSetter(List<Actor> actors) {
this.actors = actors;
}
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Actor actor = actors.get(i);
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId());
}
@Override
public int getBatchSize() {
return actors.size();
}
}
List<Actor> actors = // ... get actors from somewhere
jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
new ActorBatchPreparedStatementSetter(actors)
);
- Java
- Kotlin
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
Actor actor = actors.get(i);
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
}
public int getBatchSize() {
return actors.size();
}
});
}
// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): IntArray {
return jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
object: BatchPreparedStatementSetter {
override fun setValues(ps: PreparedStatement, i: Int) {
ps.setString(1, actors[i].firstName)
ps.setString(2, actors[i].lastName)
ps.setLong(3, actors[i].id)
}
override fun getBatchSize() = actors.size
})
}
// ... additional methods
}
如果你处理一个更新流或从文件中读取数据,你可能会有一个首选的批处理大小,但最后一个批次可能没有那么多条目。在这种情况下,你可以使用 InterruptibleBatchPreparedStatementSetter
接口,它允许你在输入源耗尽时中断批处理。isBatchExhausted
方法允许你发出批处理结束的信号。
使用对象列表进行批处理操作
JdbcTemplate
和 NamedParameterJdbcTemplate
都提供了一种替代方式来进行批量更新。你无需实现一个特殊的批量接口,而是在调用时以列表的形式提供所有参数值。框架会遍历这些值并使用内部的预处理语句设置器。API 的使用方式会有所不同,具体取决于你是否使用命名参数。对于命名参数,你需要提供一个 SqlParameterSource
数组,每个批量成员对应一个条目。你可以使用 SqlParameterSourceUtils.createBatch
便捷方法来创建这个数组,传入一个 bean 风格对象的数组(其 getter 方法对应参数)、String
键控的 Map
实例(包含对应的参数作为值),或者两者的混合。
下面的示例显示了使用命名参数的批量更新:
- Java
- Kotlin
public class JdbcActorDao implements ActorDao {
private NamedParameterTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int[] batchUpdate(List<Actor> actors) {
return this.namedParameterJdbcTemplate.batchUpdate(
"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
SqlParameterSourceUtils.createBatch(actors));
}
// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): IntArray {
return this.namedParameterJdbcTemplate.batchUpdate(
"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
SqlParameterSourceUtils.createBatch(actors));
}
// ... additional methods
}
对于使用经典 ?
占位符的 SQL 语句,你需要传入一个包含对象数组的列表,其中包含更新值。这个对象数组必须为 SQL 语句中的每个占位符提供一个条目,并且它们的顺序必须与 SQL 语句中定义的顺序相同。
以下示例与前面的示例相同,只是它使用了经典的 JDBC ?
占位符:
- Java
- Kotlin
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
List<Object[]> batch = new ArrayList<>();
for (Actor actor : actors) {
Object[] values = new Object[] {
actor.getFirstName(), actor.getLastName(), actor.getId()};
batch.add(values);
}
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
batch);
}
// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): IntArray {
val batch = mutableListOf<Array<Any>>()
for (actor in actors) {
batch.add(arrayOf(actor.firstName, actor.lastName, actor.id))
}
return jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?", batch)
}
// ... additional methods
}
我们之前描述的所有批量更新方法都会返回一个 int
数组,其中包含每个批量条目所影响的行数。这个计数是由 JDBC 驱动程序报告的。如果计数不可用,JDBC 驱动程序会返回一个值 -2
。
在这种情况下,自动设置底层 PreparedStatement
的值时,每个值的对应 JDBC 类型需要从给定的 Java 类型中推导出来。虽然这通常效果很好,但可能会出现问题(例如,Map 中包含的 null
值)。Spring 默认情况下会在这种情况下调用 ParameterMetaData.getParameterType
,这可能会对你的 JDBC 驱动程序造成负担。如果你的应用程序遇到特定的性能问题,你应该使用最新的驱动程序版本,并考虑将 spring.jdbc.getParameterType.ignore
属性设置为 true
(作为 JVM 系统属性或通过 SpringProperties 机制)。
从 6.1.2 开始,Spring 在 PostgreSQL 和 MS SQL Server 上绕过默认的 getParameterType
解析。这是一种常见的优化,旨在避免仅为了参数类型解析而进行的进一步往返数据库管理系统的操作,这在 PostgreSQL 和 MS SQL Server 上尤其显著,特别是在批量操作中。如果你发现有副作用,例如在没有特定类型指示的情况下将字节数组设置为 null,你可以显式地将 spring.jdbc.getParameterType.ignore=false
标志设置为系统属性(见上文)以恢复完整的 getParameterType
解析。
或者,你可以考虑显式指定相应的 JDBC 类型,可以通过 BatchPreparedStatementSetter
(如前所示),通过提供给 List<Object[]>
调用的显式类型数组,通过在自定义 MapSqlParameterSource
实例上调用 registerSqlType
,通过 BeanPropertySqlParameterSource
从 Java 声明的属性类型中推导出 SQL 类型,即使对于 null 值,或者通过提供单独的 SqlParameterValue
实例而不是简单的 null 值。
批量操作与多个批次
前面的批量更新示例处理的是如此大的批次,以至于您希望将它们拆分为几个较小的批次。您可以使用前面提到的方法,通过多次调用 batchUpdate
方法来实现这一点,但现在有一种更方便的方法。除了 SQL 语句之外,该方法还需要一个包含参数的 Collection
对象、每个批次要进行的更新次数,以及一个 ParameterizedPreparedStatementSetter
来设置预准备语句参数的值。框架会遍历提供的值,并将更新调用分成指定大小的批次。
以下示例展示了使用批量大小为 100 的批量更新:
- Java
- Kotlin
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[][] batchUpdate(final Collection<Actor> actors) {
int[][] updateCounts = jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
actors,
100,
(PreparedStatement ps, Actor actor) -> {
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
});
return updateCounts;
}
// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): Array<IntArray> {
return jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
actors, 100) { ps, argument ->
ps.setString(1, argument.firstName)
ps.setString(2, argument.lastName)
ps.setLong(3, argument.id)
}
}
// ... additional methods
}
此调用的批量更新方法返回一个 int
数组的数组,其中包含每个批次的数组条目,每个条目包含每次更新受影响行数的数组。顶级数组的长度表示运行的批次数量,第二级数组的长度表示该批次中的更新次数。每个批次中的更新次数应为所有批次提供的批量大小(除了最后一个可能较少的批次),这取决于提供的更新对象总数。每个更新语句的更新计数由 JDBC 驱动程序报告。如果计数不可用,JDBC 驱动程序返回 -2
的值。