JDBC Batch Operations

如果你批量调用同一个准备好的语句,大多数 JDBC 驱动程序都会提供更好的性能。通过将更新分组到批次中,你可以限制到数据库的往返次数。

使用 JdbcTemplate 的基本批处理操作

Basic Batch Operations with JdbcTemplate

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

  1. public class JdbcActorDao implements ActorDao {
  2. private JdbcTemplate jdbcTemplate;
  3. public void setDataSource(DataSource dataSource) {
  4. this.jdbcTemplate = new JdbcTemplate(dataSource);
  5. }
  6. public int[] batchUpdate(final List<Actor> actors) {
  7. return this.jdbcTemplate.batchUpdate(
  8. "update t_actor set first_name = ?, last_name = ? where id = ?",
  9. new BatchPreparedStatementSetter() {
  10. public void setValues(PreparedStatement ps, int i) throws SQLException {
  11. Actor actor = actors.get(i);
  12. ps.setString(1, actor.getFirstName());
  13. ps.setString(2, actor.getLastName());
  14. ps.setLong(3, actor.getId().longValue());
  15. }
  16. // 告诉框架需要调用 setValues 多少次
  17. public int getBatchSize() {
  18. return actors.size();
  19. }
  20. });
  21. }
  22. // ... additional methods
  23. }

如果你处理一个更新流或从文件中读取,你可能有一个首选的批处理大小,但最后一批可能没有这个数量的条目。在这种情况下,你可以使用InterruptibleBatchPreparedStatementSetter 接口,它可以让你在输入源用完后中断一个批处理。isBatchExhausted 方法让你发出批处理结束的信号(InterruptibleBatchPreparedStatementSetter 继承了 BatchPreparedStatementSetter 接口)。

  1. this.jdbcTemplate.batchUpdate(
  2. "update t_actor set first_name = ?, last_name = ? where id = ?",
  3. new InterruptibleBatchPreparedStatementSetter() {
  4. // 这里的参数是当前已经处理的语句条数,可以简单的认为是已经执行了多少条语句, 从 0 开始
  5. @Override
  6. public boolean isBatchExhausted(int i) {
  7. System.out.println("isBatchExhausted: " + i);
  8. return true;
  9. }
  10. public void setValues(PreparedStatement ps, int i) throws SQLException {
  11. ps.setString(1, "张三");
  12. ps.setString(2, "李四");
  13. ps.setLong(3, 1);
  14. System.out.println("setValues");
  15. }
  16. // 告诉框架需要调用 setValues 多少次
  17. public int getBatchSize() {
  18. System.out.println("getBatchSize");
  19. return 2;
  20. }
  21. });

上面的代码就有一个你搞不明白的点了:上面的 setValues 到底调用了几次?要搞明白这个问题,其实就要知道他们三个方法的调用时机和含义:

  1. getBatchSize : 整个批处理中只会调用一次,表示你希望调用多少次 setValues 来生成语句
  2. setValues:会按照 getBatchSize 返回的值循环调用
  3. isBatchExhausted:在每一次 setValues 后调用,如果返回 true,那么这不会有下一次调用 setValues 了,整个批处理语句会结束

所以上面的示例打印结果如下:

  1. getBatchSize
  2. setValues
  3. isBatchExhausted: 0

如果 isBatchExhausted 中始终返回 false,那么整个批处理最多调用 setValues getBatchSize 次,而不是无限次

使用对象列表的批处理操作

Batch Operations with a List of Objects

JdbcTemplate 和 NamedParameterJdbcTemplate 都提供了一种提供批量更新的替代方式。你不用实现一个特殊的批处理接口,而是在调用中以列表的形式提供所有参数值。框架在这些值上循环,并使用一个内部准备好的语句设置器。API 是不同的,取决于你是否使用命名参数。对于命名参数,你提供一个 SqlParameterSource 数组,批处理的每个成员有一个条目。你可以使用 SqlParameterSourceUtils.createBatch 方便的方法来创建这个数组,传入一个 bean 风格的对象数组(有对应于参数的 getter 方法),String-keyed Map 实例(包含对应的参数作为值),或者两者的混合。

下面的例子显示了一个使用命名参数的批量更新:

  1. public class JdbcActorDao implements ActorDao {
  2. private NamedParameterTemplate namedParameterJdbcTemplate;
  3. public void setDataSource(DataSource dataSource) {
  4. this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
  5. }
  6. public int[] batchUpdate(List<Actor> actors) {
  7. return this.namedParameterJdbcTemplate.batchUpdate(
  8. "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
  9. SqlParameterSourceUtils.createBatch(actors));
  10. }
  11. // ... additional methods
  12. }

对于使用经典占位符的 SQL 语句,你传递一个包含更新值的对象数组的列表。这个对象数组必须为 SQL 语句中的每个占位符提供一个条目,而且它们的顺序必须与 SQL 语句中的定义相同。

下面的例子与前面的例子相同,只是它使用了经典的 JDBC 占位符:

  1. public class JdbcActorDao implements ActorDao {
  2. private JdbcTemplate jdbcTemplate;
  3. public void setDataSource(DataSource dataSource) {
  4. this.jdbcTemplate = new JdbcTemplate(dataSource);
  5. }
  6. public int[] batchUpdate(final List<Actor> actors) {
  7. List<Object[]> batch = new ArrayList<Object[]>();
  8. for (Actor actor : actors) {
  9. Object[] values = new Object[] {
  10. actor.getFirstName(), actor.getLastName(), actor.getId()};
  11. batch.add(values);
  12. }
  13. return this.jdbcTemplate.batchUpdate(
  14. "update t_actor set first_name = ?, last_name = ? where id = ?",
  15. batch);
  16. }
  17. // ... additional methods
  18. }

我们前面描述的所有批处理更新方法都会返回一个 int 数组,包含每个批处理条目的受影响行数。这个计数是由 JDBC 驱动报告的。如果该计数不可用,JDBC 驱动程序会返回一个值 -2。

:::info 在这种情况下,在底层 PreparedStatement 上自动设置值,每个值对应的 JDBC 类型需要从给定的 Java 类型派生。虽然这通常运作良好,但也有可能出现问题(例如,对于包含空值的 Map)。在这种情况下,Spring 默认会调用 ParameterMetaData.getParameterType,这对你的 JDBC 驱动程序来说可能很昂贵。如果你遇到性能问题(如 Oracle 12c、JBoss 和 PostgreSQL 上报告的那样),你应该使用最新的驱动版本,并考虑将 spring.jdbc.getParameterType.ignore属性设置为 true(作为 JVM 系统属性或通过 SpringProperties 机制)。

另外,你可以考虑明确指定相应的 JDBC 类型,可以通过 BatchPreparedStatementSetter(如前所示),通过给基于 List<Object[]>的调用一个明确的类型数组,通过对自定义 MapSqlParameterSource 实例的 registerSqlType 调用,或者通过BeanPropertySqlParameterSource,从 Java 声明的属性类型中导出 SQL 类型,甚至对一个空值。 :::

多个批次的批量操作

Batch Operations with Multiple Batches

前面的批量更新的例子是处理那些大到你想把它们分成几个小的批次的批次。你可以用前面提到的方法通过多次调用 batchUpdate 方法来实现这个目的,但是现在有一个更方便的方法。这个方法除了接受 SQL 语句外,还接受一个包含参数的对象集合,每个批次的更新数量,以及一个 ParameterizedPreparedStatementSetter 来设置准备语句的参数值。该框架在所提供的值上循环,并将更新调用分成指定规模的批次。

下面的例子显示了一个使用 100 个批次大小的批次更新:

  1. public class JdbcActorDao implements ActorDao {
  2. private JdbcTemplate jdbcTemplate;
  3. public void setDataSource(DataSource dataSource) {
  4. this.jdbcTemplate = new JdbcTemplate(dataSource);
  5. }
  6. public int[][] batchUpdate(final Collection<Actor> actors) {
  7. int[][] updateCounts = jdbcTemplate.batchUpdate(
  8. "update t_actor set first_name = ?, last_name = ? where id = ?",
  9. actors,
  10. 100, // 100 条数据进行一次提交处理
  11. (PreparedStatement ps, Actor actor) -> {
  12. ps.setString(1, actor.getFirstName());
  13. ps.setString(2, actor.getLastName());
  14. ps.setLong(3, actor.getId().longValue());
  15. });
  16. return updateCounts;
  17. }
  18. // ... additional methods
  19. }

这个调用的批次更新方法返回一个 int 数组,该数组包含每个批次的一个数组条目,其中包含每个更新的受影响行数的数组。顶层数组的长度表示运行的批次数量,第二层数组的长度表示该批次中的更新数量。每个批次中的更新数量应该是为所有批次提供的批次大小(除了最后一个可能更少),这取决于提供的更新对象的总数。每个更新语句的更新计数是由 JDBC 驱动程序报告的。如果计数不可用,JDBC 驱动程序会返回一个值 -2。