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 ドライバーではコストがかかる可能性があります。パフォーマンスの問題が発生した場合(Oracle 12c、JBoss、PostgreSQL で報告されているように)、最新のドライバーバージョンを使用し、spring.jdbc.getParameterType.ignore プロパティを true に設定することを検討する必要があります(JVM システムプロパティとして、または SpringProperties メカニズムを介して)。

代わりに、対応する JDBC 型を明示的に指定することも考えられます。BatchPreparedStatementSetter (先に示したように)、List<Object[]> ベースの呼び出しに与えられる明示的な型配列、カスタム MapSqlParameterSource インスタンス上の registerSqlType 呼び出し、または NULL 値の場合でも Java で宣言されたプロパティ型から SQL 型を派生させる BeanPropertySqlParameterSource のいずれかを使用します。

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

前述のバッチ更新の例では、非常に大きいバッチを処理するため、いくつかの小さなバッチに分割します。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 の値を返します。