JDBC 批处理操作 (JDBC Batch Operations)
如果将对同一预编译语句(prepared statement)的多次调用进行批处理,大多数 JDBC 驱动程序都能提供更高的性能。通过将更新分组为批次,你可以限制与数据库的往返次数。
使用 JdbcTemplate 进行基本批处理操作
你通过实现一个特殊的接口 BatchPreparedStatementSetter 的两个方法,并将其作为第二个参数传给 batchUpdate 方法调用,来完成 JdbcTemplate 的批处理。
getBatchSize方法提供当前批次的大小。setValues方法为预编译语句的参数设置值。该方法的调用次数即为你指定的getBatchSize的值。
以下示例根据列表中的条目更新 t_actor 表,整个列表被作为一个批次使用:
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();
}
});
}
// ... 其他方法
}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
})
}
// ... 其他方法
}如果你正在处理更新流或从文件中读取,你可能会有一个首选的批次大小,但最后一个批次可能没有那么多条目。在这种情况下,你可以使用 InterruptibleBatchPreparedStatementSetter 接口,它允许你在输入源耗尽后中断批次。isBatchExhausted 方法允许你发出批次结束的信号。
使用对象列表进行批处理操作
JdbcTemplate 和 NamedParameterJdbcTemplate 都提供了另一种提供批处理更新的方法。你无需实现特殊的批处理接口,而是将所有参数值作为列表传入。框架会遍历这些值并使用内部的预编译语句设置。
API 会根据你是否使用命名参数而有所不同:
- 命名参数:你提供一个
SqlParameterSource数组。可以使用SqlParameterSourceUtils.createBatch便捷方法创建此数组。 - 经典占位符 (?):你传入一个包含对象数组(Object Array)的列表。
以下示例显示了使用命名参数的批处理更新:
public class JdbcActorDao implements ActorDao {
private NamedParameterJdbcTemplate 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));
}
// ... 其他方法
}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));
}
// ... 其他方法
}对于使用经典 ? 占位符的 SQL 语句,你传入一个包含对象数组的列表,数组中的值必须与 SQL 语句中的占位符顺序一致。
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);
}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)
}所有上述批处理更新方法都返回一个 int 数组,包含每个批次条目受影响的行数。如果该计数不可用,JDBC 驱动程序将返回 -2。
性能注意
默认情况下,Spring 会在自动设置参数时尝试获取参数元数据(getParameterType),这在某些驱动程序(如 PostgreSQL 和 MS SQL Server)上由于额外的网络往返可能会很慢。 从 6.1.2 开始,Spring 默认绕过这些数据库的元数据解析。如果你遇到相关问题,可以考虑通过系统属性 spring.jdbc.getParameterType.ignore=true 进行控制,或者显式指定 JDBC 类型。
使用多个批次进行批处理操作
如果你的数据量非常大(例如上万条),一次性作为一个大批次处理可能会导致内存压力或数据库限制。此时,你可以将数据拆分为多个较小的批次。
Spring 提供了一个便捷方法,它接受一个对象集合、每个批次的大小以及一个 ParameterizedPreparedStatementSetter。框架会自动循环这些值,并按照指定的批次大小分段执行。
以下示例显示了使用 100 为批次大小的更新:
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;
}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)
}
}该方法返回一个 int[][](二维数组)。外层数组长度表示运行了多少个批次,内层数组长度表示该批次中的更新数量。
补充教学
1. 批处理:性能提升的银弹?
在数据库操作中,最大的开销往往不是执行 SQL 本身,而是网络往返(Network Roundtrip)。 普通的 update 调用是: 程序 -> [SQL] -> 数据库 -> [确认信息] -> 程序(循环 1000 次) 批处理的调用是: 程序 -> [SQL + 1000组参数] -> 数据库 -> [1000个确认结果] -> 程序(仅 1 次网络连接) 对于远程数据库,批处理通常能带来 10 倍甚至更高的性能提升。
2. 多大的“批次”最合适?
并不是批次越大越好。批次过大会导致:
- 内存压力:驱动程序需要缓存待发送的所有数据。
- 事务锁定:大事务可能长时间锁定表行,影响并发。 经验法则:通常将批次大小设置为 50 到 100。如果数据量巨大,请利用
jdbcTemplate.batchUpdate(sql, collection, batchSize, setter)方法自动分段,这种方式既能享受批处理性能,又不会撑爆内存。
3. 理解 int[] vs int[][]
int[]:当你调用单次批处理时使用。数组的每一项对应你传入参数列表中的每一项受影响的行数。int[][]:当你让 Spring 帮你自动拆分批次时返回。例如 1000 条数据拆成 10 批,外层数组长度就是 10。 注意:有些数据库驱动在批处理模式下无法准确计算受影响行数,它们可能会统一返回-2(SUCCESS_NO_INFO)。这并不代表执行失败,只是无法告知具体数量。