Skip to content

JDBC 批处理操作 (JDBC Batch Operations)

如果将对同一预编译语句(prepared statement)的多次调用进行批处理,大多数 JDBC 驱动程序都能提供更高的性能。通过将更新分组为批次,你可以限制与数据库的往返次数。

使用 JdbcTemplate 进行基本批处理操作

你通过实现一个特殊的接口 BatchPreparedStatementSetter 的两个方法,并将其作为第二个参数传给 batchUpdate 方法调用,来完成 JdbcTemplate 的批处理。

  • getBatchSize 方法提供当前批次的大小。
  • setValues 方法为预编译语句的参数设置值。该方法的调用次数即为你指定的 getBatchSize 的值。

以下示例根据列表中的条目更新 t_actor 表,整个列表被作为一个批次使用:

java
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();
					}
				});
	}

	// ... 其他方法
}
kotlin
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 方法允许你发出批次结束的信号。

使用对象列表进行批处理操作

JdbcTemplateNamedParameterJdbcTemplate 都提供了另一种提供批处理更新的方法。你无需实现特殊的批处理接口,而是将所有参数值作为列表传入。框架会遍历这些值并使用内部的预编译语句设置。

API 会根据你是否使用命名参数而有所不同:

  • 命名参数:你提供一个 SqlParameterSource 数组。可以使用 SqlParameterSourceUtils.createBatch 便捷方法创建此数组。
  • 经典占位符 (?):你传入一个包含对象数组(Object Array)的列表。

以下示例显示了使用命名参数的批处理更新:

java
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));
	}

	// ... 其他方法
}
kotlin
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 语句中的占位符顺序一致。

java
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);
}
kotlin
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 为批次大小的更新:

java
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;
}
kotlin
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. 多大的“批次”最合适?

并不是批次越大越好。批次过大会导致:

  1. 内存压力:驱动程序需要缓存待发送的所有数据。
  2. 事务锁定:大事务可能长时间锁定表行,影响并发。 经验法则:通常将批次大小设置为 50 到 100。如果数据量巨大,请利用 jdbcTemplate.batchUpdate(sql, collection, batchSize, setter) 方法自动分段,这种方式既能享受批处理性能,又不会撑爆内存。

3. 理解 int[] vs int[][]

  • int[]:当你调用单次批处理时使用。数组的每一项对应你传入参数列表中的每一项受影响的行数。
  • int[][]:当你让 Spring 帮你自动拆分批次时返回。例如 1000 条数据拆成 10 批,外层数组长度就是 10。 注意:有些数据库驱动在批处理模式下无法准确计算受影响行数,它们可能会统一返回 -2 (SUCCESS_NO_INFO)。这并不代表执行失败,只是无法告知具体数量。

Based on Spring Framework.