JDBC 批处理操作

大多数 JDBC 驱动程序在对同一个预处理语句进行多次调用时,如果采用批处理方式,可以提高性能。通过将更新操作分组到批次中,可以限制对数据库的往返次数。

JdbcTemplate 的基本批处理操作

您可以通过实现一个特殊接口 BatchPreparedStatementSetter 的两个方法,并将该实现作为 batchUpdate 方法调用的第二个参数传入,来完成 JdbcTemplate 批处理。您可以使用 getBatchSize 方法提供当前批次的大小。您可以使用 setValues 方法设置预处理语句参数的值。此方法会按照您在 getBatchSize 调用中指定的次数被调用。以下示例根据列表中的条目更新 t_actor 表,整个列表作为批次使用:

  • 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 方法允许您发出批处理结束的信号。

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

JdbcTemplateNamedParameterJdbcTemplate 都提供了另一种提供批处理更新的方式。您无需实现特殊的批处理接口,而是在调用中以列表形式提供所有参数值。框架会遍历这些值并使用内部的预处理语句设置器。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 解析。这是一个常见的优化,旨在避免为了参数类型解析而进一步往返于 DBMS,这在 PostgreSQL 和 MS SQL Server 上尤其能带来显著差异,特别是在批处理操作中。如果您碰巧看到副作用,例如在没有特定类型指示的情况下将字节数组设置为 null,您可以明确地将 spring.jdbc.getParameterType.ignore=false 标志设置为系统属性(参见上文)以恢复完整的 getParameterType 解析。 或者,您可以考虑明确指定相应的 JDBC 类型,可以通过 BatchPreparedStatementSetter(如前所示),通过提供给基于 List<Object[]> 调用的显式类型数组,通过自定义 MapSqlParameterSource 实例上的 registerSqlType 调用,通过 BeanPropertySqlParameterSource(即使对于 null 值也能从 Java 声明的属性类型派生 SQL 类型),或者通过提供单独的 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