JDBC バッチ操作

ほとんどの JDBC ドライバーは、同じ準備済みステートメントへの複数の呼び出しをバッチ処理すると、パフォーマンスが向上します。更新をバッチにグループ化することにより、データベースへのラウンドトリップの回数を制限します。

JdbcTemplate を使用した基本的なバッチ操作

JdbcTemplate バッチ処理を実現するには、特別なインターフェース BatchPreparedStatementSetter の 2 つのメソッドを実装し、その実装を batchUpdate メソッド呼び出しの 2 番目のパラメーターとして渡します。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 メソッドを使用すると、バッチの終了を通知できます。

オブジェクトのリストを使用したバッチ操作

JdbcTemplate と NamedParameterJdbcTemplate の両方が、バッチ更新を提供する代替メソッドを提供します。特別なバッチインターフェースを実装する代わりに、呼び出しのすべてのパラメーター値をリストとして提供します。フレームワークはこれらの値をループ処理し、内部準備済みステートメント setter を使用します。API は、名前付きパラメーターを使用するかどうかによって異なります。名前付きパラメーターに対して、SqlParameterSource の配列を提供します。これは、バッチのメンバーごとに 1 つのエントリです。SqlParameterSourceUtils.createBatch 簡易メソッドを使用してこの配列を作成し、Bean スタイルオブジェクトの配列(getter メソッドがパラメーターに対応する)、String -keyed 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 ステートメントのプレースホルダーごとに 1 つのエントリが必要であり、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 型から派生する必要があります。これは通常うまく機能しますが、問題が発生する可能性があります (たとえば、マップに含まれる 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 呼び出し、null 値の場合であっても Java 宣言プロパティ型から SQL 型を派生する BeanPropertySqlParameterSource、または単純な null 値の代わりに個別の SqlParameterValue インスタンスを提供することのいずれかの方法があります。

複数のバッチを使用したバッチ操作

前述のバッチ更新の例では、非常に大きいバッチを処理するため、いくつかの小さなバッチに分割します。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 配列の配列を返します。最上位の配列の長さは実行されたバッチの数を示し、第 2 レベルの配列の長さはそのバッチの更新の数を示します。各バッチの更新の数は、提供された更新オブジェクトの総数に応じて、すべてのバッチに提供されたバッチサイズ(最後のバッチより少ない場合を除く)である必要があります。各更新ステートメントの更新カウントは、JDBC ドライバーによって報告されたものです。カウントが使用できない場合、JDBC ドライバーは -2 の値を返します。