バッチ分割は「一気にやると危ない処理を、小さな塊に分けて安全に回す」技
バッチ分割は、ざっくり言うと
「大量データを、バッチ(かたまり)単位に分けて処理する」ためのユーティリティです。
1 万件のレコードを一度に DB に insert すると重すぎる。
外部 API が「1 回 100 件まで」と制限している。
メール送信を 1 件ずつやると遅いので、50 件ずつまとめて処理したい。
こういうときに、「バッチ分割」があるかどうかで、
コードの安全性・読みやすさ・チューニングのしやすさが大きく変わります。
基本イメージ:List を「バッチサイズごと」に刻む
まずはシンプルなバッチ分割ユーティリティ
分割処理そのものは、前にやった「分割処理」とほぼ同じです。
ただし、ここでは「バッチ」という名前で、より“業務寄りの意味”を持たせます。
import java.util.ArrayList;
import java.util.List;
public final class Batches {
private Batches() {}
public static <T> List<List<T>> split(List<T> source, int batchSize) {
if (source == null || source.isEmpty()) {
return List.of();
}
if (batchSize <= 0) {
throw new IllegalArgumentException("batchSize must be > 0");
}
List<List<T>> result = new ArrayList<>();
int total = source.size();
for (int i = 0; i < total; i += batchSize) {
int toIndex = Math.min(i + batchSize, total);
result.add(source.subList(i, toIndex));
}
return result;
}
}
Javaここでの重要ポイントは三つです。
一つ目は、「batchSize ごとにインデックスを刻んでいる」ことです。i += batchSize というループが、「バッチ単位で進む」ことを表しています。
二つ目は、「最後のバッチは batchSize 未満でもよい」としていることです。
103 件を 50 件ずつに分けると、50・50・3 になりますが、それで正しい、というルールです。
三つ目は、「batchSize <= 0 を明示的に弾いている」ことです。
バッチサイズ 0 は論理的にありえないので、早めに例外で気づけるようにします。
業務例1:外部 API に 100 件ずつ送る
「バッチ分割+for」で素直に書ける形にする
よくあるのが、「外部 API の制限で、一度に送れる件数が決まっている」ケースです。
public class SendToApiSample {
public static void main(String[] args) {
List<String> ids = /* 1 万件くらいの ID 一覧 */ List.of();
for (List<String> batch : Batches.split(ids, 100)) {
sendToExternalApi(batch);
}
}
private static void sendToExternalApi(List<String> idBatch) {
// ここで外部 API 呼び出し
// 例:POST /bulk { "ids": [...] }
}
}
Javaここでの重要ポイントは、
「呼び出し側のコードが“100 件ずつ送る”という意図だけを素直に書けている」ことです。
Batches.split(ids, 100) という一行が、
「大量の ID を 100 件単位のバッチに刻む」という業務ルールを表現しています。
for の中では「バッチ単位で送る」ことだけに集中できます。
業務例2:DB バルク insert を 1000 件ずつに分ける
トランザクションやロック時間を抑えるためのバッチ分割
DB に大量 insert するときも、バッチ分割はよく使います。
public class BulkInsertSample {
public static void main(String[] args) {
List<UserEntity> entities = /* 何万件かのデータ */ List.of();
for (List<UserEntity> batch : Batches.split(entities, 1000)) {
insertBatch(batch);
}
}
private static void insertBatch(List<UserEntity> batch) {
// ここでバルク insert
// 例:INSERT INTO ... VALUES (...), (...), ...
}
}
Javaここでの重要ポイントは二つです。
一つ目は、「バッチサイズがそのまま“DB にかける負荷の単位”になっている」ことです。
1000 件ずつにすれば、トランザクション時間やロック時間をある程度コントロールできます。
二つ目は、「バッチサイズを定数や設定値にしておくと、あとからチューニングしやすい」ことです。
public static final int USER_INSERT_BATCH_SIZE = 1000;
Javaのようにしておけば、
「本番では 500 件に落とそう」といった調整が簡単になります。
subList の“ビュー”問題とコピー版バッチ
「バッチの中身を後で変更するか?」を意識する
subList は「元の List の一部を切り出したビュー」です。
つまり、元の List と要素を共有しています。
バッチの中身を変更しない(読み取り専用)なら問題になりにくいですが、
バッチごとに remove したり、sort したりするなら、
コピーを作っておく方が安全です。
public static <T> List<List<T>> splitCopy(List<T> source, int batchSize) {
if (source == null || source.isEmpty()) {
return List.of();
}
if (batchSize <= 0) {
throw new IllegalArgumentException("batchSize must be > 0");
}
List<List<T>> result = new ArrayList<>();
int total = source.size();
for (int i = 0; i < total; i += batchSize) {
int toIndex = Math.min(i + batchSize, total);
result.add(new ArrayList<>(source.subList(i, toIndex)));
}
return result;
}
Javaここでの重要ポイントは、
「バッチを“読み取り専用で使うのか”“中身をいじるのか”を最初に決める」ことです。
実務では、
「基本は読み取り専用の split を使う」
「中身をいじる必要がある処理だけ splitCopy を使う」
といったルールにしておくと、安全で分かりやすくなります。
バッチ分割とエラーハンドリング
「どのバッチで失敗したか」を追えるようにする
バッチ処理で大事なのが、「どのバッチで失敗したか」を特定できることです。
public class RobustBatchSample {
public static void main(String[] args) {
List<String> ids = /* 大量の ID */ List.of();
List<List<String>> batches = Batches.split(ids, 100);
for (int i = 0; i < batches.size(); i++) {
List<String> batch = batches.get(i);
try {
sendToExternalApi(batch);
} catch (Exception e) {
System.err.println("バッチ " + i + " で失敗。件数=" + batch.size());
// ログ出力やリトライキューへの登録など
}
}
}
}
Javaここでの重要ポイントは二つです。
一つ目は、「バッチのインデックス(何番目のバッチか)をログに残している」ことです。
これにより、「全体のうちどの範囲で失敗したか」がすぐ分かります。
二つ目は、「バッチ単位で try-catch している」ことです。
1 件の失敗で全体が止まるのではなく、
「このバッチは失敗したが、他のバッチは処理を続ける」といった制御がしやすくなります。
バッチ分割と並列処理の組み合わせ
「バッチ単位で並列に回す」という発想
バッチ分割は、並列処理と相性がいいです。
public class ParallelBatchSample {
public static void main(String[] args) {
List<String> ids = /* 大量の ID */ List.of();
Batches.split(ids, 100)
.parallelStream()
.forEach(ParallelBatchSample::sendToExternalApi);
}
private static void sendToExternalApi(List<String> batch) {
// バッチ単位の処理
}
}
Javaここでの重要ポイントは、
「並列化の単位を“1 件”ではなく“バッチ”にしている」ことです。
1 件ずつ並列にするとオーバーヘッドが大きくなりすぎますが、
100 件単位で並列にすると、
「そこそこ大きい仕事を、複数スレッドで分担する」形になり、効率がよくなります。
バッチサイズは、そのまま「並列処理の粒度」を意味します。
ここをユーティリティ+定数で管理しておくと、
負荷とスループットのバランス調整がしやすくなります。
まとめ:バッチ分割ユーティリティで身につけてほしい感覚
バッチ分割は、
単に「List を小分けにするテクニック」ではなく、
「大量データを、安全に・制御可能な単位で処理するための設計」です。
Batches.split(list, batchSize) のようなユーティリティで、「バッチサイズ」という業務ルールを一箇所に閉じ込める。
最後のバッチはサイズ未満でもよい、という仕様を明確にしておく。
subList がビューであることを理解し、「読み取り専用か、コピーが必要か」を意識して使い分ける。
バッチ単位でエラーハンドリングし、「どのバッチで失敗したか」を追えるようにする。
並列処理・外部 API・DB バルク処理などの「処理単位」を決める道具として、バッチ分割を位置づける。
もしあなたのコードのどこかに、
「for 文でインデックスをいじりながら、手作業で 100 件ずつに分けて API を叩いている」箇所があれば、
それを一度「バッチ分割ユーティリティ」に置き換えられないか眺めてみてください。
その小さな整理が、
「大量データを、落ち着いてさばけるエンジニア」への、
確かな一歩になります。
