JDBC 批量操作

如果对同一个预处理语句(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();
          }
        });
  }

  // ... 其他方法
}

如果您正在处理更新流或从文件读取,您可能有一个首选的批次大小,但最后一个批次可能没有那么多条目。 在这种情况下,您可以使用 InterruptibleBatchPreparedStatementSetter 接口,它允许您在输入源耗尽后中断批次。 isBatchExhausted 方法允许您发出批次结束的信号。

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

JdbcTemplateNamedParameterJdbcTemplate 都提供了另一种提供批量更新的方式。 无需实现特殊的批处理接口,您可以将所有参数值作为列表在调用中提供。框架会循环遍历这些值并使用内部预处理语句设置器。 API 取决于您是否使用命名参数。对于命名参数,您提供一个 SqlParameterSource 数组,批次中的每个成员对应一个条目。 您可以使用 SqlParameterSourceUtils.createBatch 便捷方法创建此数组,传入一组 bean 风格的对象 (带有对应于参数的 getter 方法)、以 String 为键的 Map 实例(包含对应参数作为值),或者两者的混合。

以下示例展示了使用命名参数的批量更新:

  • Java

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

  // ... 其他方法
}

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

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

  • 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) {
    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);
  }

  // ... 其他方法
}

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

在这样的场景中,随着底层 PreparedStatement 上值的自动设置,每个值的相应 JDBC 类型需要从给定的 Java 类型派生。 虽然这通常工作正常,但也存在潜在的问题(例如,对于 Map 包含的 null 值)。 在这种情况下,Infra 默认调用 ParameterMetaData.getParameterType,这对于您的 JDBC 驱动程序来说可能开销很大。 如果您遇到性能问题(如在 Oracle 12c、JBoss 和 PostgreSQL 上报告的那样),您应该使用最近的驱动程序版本, 并考虑将 jdbc.getParameterType.ignore 属性设置为 true(作为 JVM 系统属性或通过 TodayStrategies 机制)。

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

多批次批量操作

前面的批量更新示例处理的是非常大的批次,以至于您希望将它们分解为几个较小的批次。 您可以使用前面提到的方法通过多次调用 batchUpdate 方法来做到这一点,但现在有一个更方便的方法。 此方法除了 SQL 语句外,还需要一个包含参数的对象 Collection、每个批次的更新次数, 以及一个用于设置预处理语句参数值的 ParameterizedPreparedStatementSetter。 框架循环遍历提供的值,并将更新调用分解为指定大小的批次。

以下示例展示了使用批次大小为 100 的批量更新:

  • Java

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

  // ... 其他方法
}

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