对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

JDBC 批处理操作

大多数 JDBC 驱动程序在对同一预编译语句(prepared statement)进行多次调用时,如果将这些调用批量处理,可以显著提升性能。通过将更新操作分组为批次,可以减少与数据库之间的往返次数。spring-doc.cadn.net.cn

使用 进行基本批量操作JdbcTemplate

您可以通过实现一个特殊接口 JdbcTemplate 的两个方法,并将该实现作为第二个参数传递给 BatchPreparedStatementSetter 方法调用来完成 batchUpdate 的批处理操作。您可以使用 getBatchSize 方法来指定当前批次的大小,使用 setValues 方法为预编译语句(prepared statement)的参数设置值。该方法会被调用的次数等于您在 getBatchSize 方法中指定的次数。以下示例根据一个列表中的条目更新 t_actor 表,并且整个列表被用作一个批次:spring-doc.cadn.net.cn

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 方法可用于指示批次的结束。spring-doc.cadn.net.cn

使用对象列表进行批量操作

JdbcTemplateNamedParameterJdbcTemplate 都提供了另一种执行批量更新的方式。你无需实现特殊的批量接口,而是在调用时以列表形式提供所有参数值。框架会遍历这些值,并使用内部的预编译语句(PreparedStatement)设置器进行处理。该 API 的具体形式取决于你是否使用命名参数。对于命名参数,你需要提供一个 SqlParameterSource 数组,其中每个元素对应批量操作中的一个成员。你可以使用 SqlParameterSourceUtils.createBatch 工具方法来创建这个数组,传入一个由 bean 风格对象(其 getter 方法与参数名对应)、以 String 为键的 Map 实例(包含对应的参数值),或者两者的混合组成的数组。spring-doc.cadn.net.cn

以下示例展示了使用命名参数进行批量更新:spring-doc.cadn.net.cn

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 语句中定义的顺序相同。spring-doc.cadn.net.cn

以下示例与前面的示例相同,只是它使用了经典的 JDBC ? 占位符:spring-doc.cadn.net.cn

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 驱动程序将返回值 -2spring-doc.cadn.net.cn

在这种情况下,当在底层的 PreparedStatement 上自动设置值时,需要为每个值根据给定的 Java 类型推导出相应的 JDBC 类型。虽然这通常能正常工作,但可能存在潜在问题(例如,对于包含在 Map 中的 null 值)。默认情况下,Spring 在这种情况下会调用 ParameterMetaData.getParameterType,这在您的 JDBC 驱动程序中可能开销较大。如果您的应用程序遇到特定的性能问题,您应该使用较新版本的驱动程序,并考虑将 spring.jdbc.getParameterType.ignore 属性设置为 true(作为 JVM 系统属性,或通过 SpringProperties 机制)。spring-doc.cadn.net.cn

或者,您也可以考虑显式指定相应的 JDBC 类型, 可以通过 BatchPreparedStatementSetter(如前所示)、 通过向基于 List<Object[]> 的调用传入显式的类型数组、 通过在自定义的 registerSqlType 实例上调用 MapSqlParameterSource 方法、 通过使用 BeanPropertySqlParameterSource(即使属性值为 null,也能从 Java 声明的属性类型推导出 SQL 类型), 或者通过提供单独的 SqlParameterValue 实例来替代普通的 null 值。spring-doc.cadn.net.cn

使用多个批次进行批量操作

前面的批量更新示例处理的是规模非常大的批次,你可能希望将其拆分为若干个较小的批次。你可以通过多次调用之前提到的 batchUpdate 方法来实现这一点,但现在有一种更为便捷的方法。该方法除了接收 SQL 语句外,还接收一个包含参数的对象 Collection、每个批次要执行的更新数量,以及一个 ParameterizedPreparedStatementSetter 用于设置预编译语句中的参数值。框架会遍历所提供的参数值,并将更新调用按指定的批次大小进行拆分。spring-doc.cadn.net.cn

以下示例展示了一个使用批处理大小为100的批量更新:spring-doc.cadn.net.cn

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 驱动程序将返回值 -2spring-doc.cadn.net.cn