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驱动程序性能下降。如果遇到性能问题(如在Oracle 12c、JBoss和PostgreSQL上报告的问题),您应该使用最新的驱动程序版本,并考虑将spring.jdbc.getParameterType.ignore属性设置为true(作为JVM系统属性或通过SpringProperties机制)。

或者,您可以考虑显式指定相应的JDBC类型,可以通过BatchPreparedStatementSetter(如前面所示)、通过给定给基于List<Object[]>的调用的显式类型数组、通过在自定义MapSqlParameterSource实例上调用registerSqlType方法、或通过BeanPropertySqlParameterSource从Java声明的属性类型派生SQL类型,即使对于空值也是如此。

使用多个批处理的批处理操作

前面的批处理更新示例涉及到批处理非常大,以至于您希望将它们分成几个较小的批次。您可以通过多次调用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