Spring BatchでつくるCSVを複数ファイルに分割する
0. 読み書きあれこれ
他のシステムから送られてきたCSVを読み込んでDBに書き込む、あるあるですね?
逆に他のシステムへデータを送るため、DBのレコードをかき集めてCSVに固める、これもあるあるですね?
そこでもし、送り先のシステムが「CSVを一度に読める行数が決まっている」と言い出したら?
それもこれも、Spring Batchでできちゃうのです。
今日は、そんなときどうするかを覗いてみることにしましょう~(CV:関智一)
1. Readerがあるなら...?
以前Spring Batchで複数ファイルをItemReaderに使うという記事でMultiResourceItemReaderについて書いた。
で、「ReaderがあるならWriterもあるやろ?天下のSpringなんだし」とググったら、あった。MultiResourceItemWriter。
2. MultiResourceItemWriter
Wraps a ResourceAwareItemWriterItemStream and creates a new output resource when the count of items written in current resource exceeds setItemCountLimitPerResource(int).
(ResourceAwareItemWriterItemStreamをラップし、現在のリソースに書き込まれたアイテムのカウントがsetItemCountLimitPerResource(int)を超えた場合に新しい出力リソースを作成する)とあるとおり、書き込み行上限に達したら新たなResourceを生成して配下のItemWriterに移譲する。
使い方はMultiResourceItemReaderとほぼ同じ。
とはいえ、書き込み先リソースの供給が必要になるItemWriterといったら、ほぼFlatFileItemWriter専用みたいな印象。
処理を移譲されるItemWriterの中身はごく普通のFlatFileItemWriterでいいので、説明は割愛。MultiResourceItemReader同様、setResourceで出力先を定める必要はない。
@Bean
public MuiltiResourceItemWriter<OrgFile> multiResOrgWriter(FlatFileItemWriter<OrgFile> orgWriter) {
// ここで指定するResourceは出力先ファイル名のひな型になる
Resource res = new FileSystemResource("/path/to/output/file.csv");
MultiResouceItemWriter<OrgFile> writer = new MultiResouceItemWriter<>();
// 実際に処理するwriter
writer.setDelegate(orgWriter);
// 出力リソース
writer.setResource(res);
// setItemCountLimitPerResource で出力ファイルごとの出力行数の基準値を定める
writer.setItemCountLimitPerResource(100);
// ジョブを再起動したとき処理を再開させたい場合はtrueにする
writer.setSaveState(true);
return writer;
}
こういったBean定義をバッチの設定ソースに仕込んだ状態でジョブを実行すると、setItemCountLimitPerResourceの設定値からチャンクサイズの間でちゃんとファイルが分割されて出力される。
3. カスタマイズ
とは書いたのだが、このままだとこんな感じでファイルができているはず。
file.csv.1
file.csv.2
file.csv.3
まぁ、UNIX系OSのみを対象にしているのであれば何ら問題はないはずなのだが、拡張子をもとにファイルタイプを特定するWindowsのようなOSだと、「わけのわからんファイルがいっぱいできている」と騒ぎになりかねない。
このファイル名は、MultiResourceItemWriterがResourceSuffixCreatorのデフォルト実装であるSimpleResourceSuffixCreatorが出力する.1
のような文字列を単にResourceのPathに連結しているだけなので、ResourceSuffixCreatorの実装内でファイル名そのものを組み立ててやればよい。
例えば、file_20230523_001.csv
みたいな書式でファイル名を設定したいときは、こんな感じになる。
@Bean
public MuiltiResourceItemWriter<OrgFile> multiResOrgWriter(FlatFileItemWriter<OrgFile> orgWriter, ResourceSuffixCreator resoureSuffixCreator) {
// ここで指定するResourceは出力先ディレクトリだけ定義しておく
Resource res = new FileSystemResource("/path/to/output/");
MultiResouceItemWriter<OrgFile> writer = new MultiResouceItemWriter<>();
writer.setDelegate(orgWriter);
writer.setResource(res);
writer.setItemCountLimitPerResource(100);
writer.setSaveState(true);
// ファイル名を実際に決めるResourceSuffixCreator
writer.setResourceSuffixCreator(resourceSuffixCreator);
return writer;
}
@Bean
public ResourceSuffixCreator resoureSuffixCreator() {
return new ResourceSuffixCreator() {
@Override
public String getSuffix(int index) {
DateFormat df = new SimpleDateFormat("yyyyMMdd");
// 添え字が渡されるのでそれを使ってファイル名そのものをここで作る
return "file_" + df.format(Date.from(Instant.now())) + String.format("_%03d", index) + ".csv";
}
}
}
こうすることで、出力結果はこうなっているはず。
file_20230523_001.csv
file_20230523_002.csv
file_20230523_003.csv
肝心の1ファイル当たりの行数だが、MultiResourceItemWriterのJavaDocに
Note that new resources are created only at chunk boundaries i.e. the number of items written into one resource is between the limit set by setItemCountLimitPerResource(int) and (limit + chunk size).
とあるとおり、
- 新しいリソース(≒ファイル)はチャンクの境界で作成される
- 実際に書き込まれる行数はsetItemCountLimitPerResource(int)で設定したリミットとリミット+チャンクサイズの間で区切られる
ことに注意。
最近のコメント