Springでバルク更新・一括挿入を高速に!flush・clearの使い方と最適化のベストプラクティス
生徒
「Spring Bootで大量データを一括登録したいんですけど、普通にsave()を繰り返すだけじゃダメなんですか?」
先生
「確かにそれでも動くけど、パフォーマンスは落ちるし、メモリも無駄に使ってしまいます。flush()やclear()を使って、効率的に処理する方法があるんですよ。」
生徒
「flushとclearって聞いたことはあるけど、いまいち意味がわからなくて…」
先生
「それなら、一括挿入やバルク更新の注意点とあわせて、パフォーマンスを上げるベストプラクティスを丁寧に解説していきましょう!」
1. Springにおけるバルク処理とは?
バルク処理とは、大量のレコードを一括で登録・更新・削除する処理のことです。Spring Data JPAでも、通常のsave()やdelete()メソッドを使ってバルク処理はできますが、ループ処理のまま実行すると性能が大幅に劣化します。
それは、エンティティがすべてPersistenceContextに保持され、メモリ消費が増えるためです。さらに、flushのタイミングが遅れることで、データベースへの反映が遅延することもあります。
2. flushとclearの基本:高速化の鍵
flushは、エンティティの変更を即座にデータベースに反映させる命令です。clearは、エンティティマネージャが管理しているキャッシュ(1次キャッシュ)をクリアしてメモリを解放する命令です。
これらを適切に組み合わせることで、一括挿入やバルク更新時のパフォーマンスを飛躍的に向上させることができます。
3. saveAllだけでは不十分?バルク挿入の落とし穴
Spring Data JPAのsaveAll()は便利ですが、内部ではEntityManager.persist()が1件ずつ呼ばれており、flushもclearも自動で呼ばれないため、大量データには向いていません。
そこで、batchSizeごとにflush()とclear()を呼ぶように手動で制御する必要があります。
@Autowired
private EntityManager entityManager;
@Transactional
public void bulkInsert(List<User> users) {
for (int i = 0; i < users.size(); i++) {
entityManager.persist(users.get(i));
if (i % 50 == 0) {
entityManager.flush();
entityManager.clear();
}
}
}
このように、一定件数ごとに明示的にflush/clearを行うことで、メモリリーク防止・性能向上を両立できます。
4. バルク更新の注意点と副作用
JPAでは、JPQLで直接UPDATE文を発行するバルク更新が可能ですが、この場合PersistenceContextの内容と実際のデータベースの状態が一致しなくなります。
@Modifying
@Query("UPDATE User u SET u.status = 'ACTIVE' WHERE u.lastLogin < :threshold")
int activateUsers(@Param("threshold") LocalDateTime threshold);
このようなバルク更新を行ったあとに、同じエンティティを参照しようとすると古い値が返ることがあるため、必要に応じてclear()や再取得が必要です。
5. 高速化のためのJPAプロパティ設定
JPAのパフォーマンスを最適化するために、Hibernateのbatchサイズを設定するのが効果的です。
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
これにより、Hibernateが内部的にバッチ処理を最適化して、SQLの発行回数を大幅に削減してくれます。
6. Repository vs EntityManager:どちらを使うべき?
Spring DataのCrudRepositoryやJpaRepositoryはシンプルで使いやすいですが、バルク処理の最適化にはEntityManagerの細かい制御が有効です。
エンティティが大量にある場面や、flushタイミングを自分で調整したいケースでは、EntityManagerを使ったほうが効率的です。
一方、通常の登録処理や小規模な更新であれば、save()やsaveAll()の使用で問題ありません。
7. バルク削除の実装と注意点
一括削除についても、deleteAll()はあまり効率が良くなく、JPQLでDELETE文を直接実行した方が速い場合があります。
@Modifying
@Query("DELETE FROM User u WHERE u.status = 'INACTIVE'")
int deleteInactiveUsers();
ただし、この方法もPersistenceContextと同期が取れなくなるため、必要ならflushやclearで整合性を保つのが重要です。
8. バルク処理でトランザクションと例外に注意
大量データを扱うと、途中で例外が発生したときのロールバックにも気をつける必要があります。
@Transactionalを付けて一括で処理する場合、どこかで例外が発生すると、すべてロールバックされるため、適切な単位でトランザクションを分割するのが安全です。
リトライ処理や、障害発生時のログ記録などもあらかじめ設計に組み込んでおきましょう。