⇧ amazing...
Spring Batchとは
公式のドキュメントによると、
A lightweight, comprehensive batch framework designed to enable the development of robust batch applications vital for the daily operations of enterprise systems.
⇧ 軽量で包括的なバッチフレームワークですと。
GitHubで公開されてる説明だと、
Spring Batch is a lightweight, comprehensive batch framework designed to enable the development of robust batch applications vital for the daily operations of enterprise systems. Spring Batch builds upon the productivity, POJO-based development approach, and general ease of use capabilities people have come to know from the Spring Framework, while making it easy for developers to access and leverage more advanced enterprise services when necessary.
⇧ 使いやすさに基づいているらしい...本当かいな...
で、アーキテクチャはと言うと、
⇧ う~む、全く分からん...
で、Spring Batchの旧いバージョンのドキュメントだと、
⇧ ありがちなバッチの構成みたいなものが描かれてますと。
このうち、
⇧ Spring Batchが関係してそうなのが、上図のアイテムってことになるんかな?
Spring BootでSpring BatchをMyBatisと一緒に使ってみる
とりあえず、使ってみますか。
⇧ 上記サイト様にプロジェクトの構造が紹介されてるけども、よく分からん...
⇧ 上記サイト様を参考にSpring Batchを実装してみる。
Spring BatchでMyBatisを使う場合、
MyBatis-Spring 1.1.0 以降では、 Spring Batch を構築するための Bean として MyBatisPagingItemReader
、 MyBatisCursorItemReader
、 MyBatisBatchItemWriter
が用意されています。
また、2.0.0 以降では、Java Configuration をサポートするための Builder クラスとして MyBatisPagingItemReaderBuilder
、 MyBatisCursorItemReaderBuilder
、 MyBatisBatchItemWriterBuilder
が用意されています。
⇧ 専用のクラスが用意されてる模様なんですが、
MyBatisCursorItemReader
This bean is an ItemReader
that reads records from a database using a cursor.
⇧ 用途がデータベースに限定されてしまうのはいかがなものか...
入力がデータベース以外のファイルとかの場合は、
フラットファイルは、最大 2 次元(表)データを含む任意の型のファイルです。Spring Batch フレームワークでのフラットファイルの読み取りは、FlatFileItemReader
と呼ばれるクラスによって促進されます。このクラスは、フラットファイルの読み取りと解析のための基本的な機能を提供します。
⇧ Spring BatchのFlatFileItemReaderを使えってことらしい。
と思ったら、
そういう時にはCSVを読み込むカスタムReaderを作るべしということだそうなので、実際にやってみた。
⇧ 何やら、CSVファイルの内容によっては、FlatFileItemReaderが上手く処理できないことが起こり得るらしく、
SpringBatchでファイルを読むと言えばまず選択肢に上がるのはFlatFileReaderかと思います。
DelimitedLineTokenizerを使ってこのクラスでcsvを読み込むサンプルもググるといくつか出て来ますが、実案件では改行や区切り文字を含む文字列にも対応しなければいけないこともあります。
そういった場合はcsv読み込みするカスタムReaderを作りましょう。
⇧ CSVファイルを扱う場合は、FlatFileItemReaderは使い物にならないことがあるそうな...
あと、「MyBatis Spring」を使う場合は依存関係の追加が必要みたいなんだけど、search.maven.org
⇧ 「MyBatis Spring」は「MyBatis Spring Boot Starter」の依存関係に含まれてるらしい...紛らわしい。
ちなみに、Spring Batchを使わずとも、
NOTE ここで扱うのは Spring Batch を使ったバッチ処理で、MyBatis の SqlSession
を利用したバッチ処理ではありません。
⇧ MyBatisのみでもバッチ処理はできるらしく、
MyBatisでもExecutorType.BATCH
を設定したSqlSession
を利用することでバッチ処理ができる。
⇧ 上記サイト様でもSpring BatchなしでMyBatisのみを利用してバッチ処理をしてる模様。
By the way、ORM(Object Relational Mapping)が、MyBatisではなくJPA(Java Persistence API)の場合は、Spring Batch用の専用クラスみたいなものは無いっぽい、多分。
話が脱線しましたが、
⇧ 上記サイト様を参考にSpring BatchをMyBatisと一緒に使ってみます。
プロジェクトは、
⇧ 前回の記事で利用したものを使っていきます。
まずは、Spring Batchの依存関係と、CSVファイル読み込むためのライブラリをbuild.gradleに追加で。
■/mybatis-example/build.gradle
plugins { id 'org.springframework.boot' version '2.7.5' id 'io.spring.dependency-management' version '1.0.15.RELEASE' id 'java' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' // https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter implementation group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '2.2.2' // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-batch implementation group: 'org.springframework.boot', name: 'spring-boot-starter-batch', version: '2.7.5' implementation 'com.opencsv:opencsv:5.7.1' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() }
そしたらば、Spring Batchでデータベースを利用する場合に、Spring Batch用のテーブルを作成する必要があるらしいので、
⇧ 上記サイト様から、自分が使用しているデータベース向けのSQLファイルを取得して実行することでテーブルを作成できるようです。
ただ、
Spring Batchのバージョンによって、テーブルのカラムが増えることがある模様。
⇧ 上記サイト様の説明にあるように、ブランチがmainが最新ではない?っぽい。
自分の環境では、PostgreSQLを利用しているので、「schema-postgresql.sql」をダウンロードします。
ダウンロード先のフォルダを作成。
ダウンロード先が決まったら、curlコマンドでダウンロード。
curl --output [ダウンロード先] https://raw.githubusercontent.com/spring-projects/spring-batch/4.3.x/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-postgresql.sql
ダウンロードできました。
A5:SQL Mk-2だと、外部ファイルの実行はできないようで、
⇧ 「A5:SQL Mk-2 コマンドラインユーティリティ」ってのを別途ダウンロードする必要がある模様。
今回は、
⇧ 上記サイト様を参考に、PostgreSQLのコマンドでSQLファイルを実行しました。
Spring Batch用のテーブルが作成できてるのが確認できました。
で、Spring BatchによるCSVファイルの読み込みを試してみたのですが、読み込むCSVファイルが駄目なのか、
com.opencsv.exceptions.CsvMalformedLineException: Unterminated quoted field at end of CSV line. Beginning of lost text: [ ]
⇧ ってエラーになって、CSVファイルの1つ目のデータしかINSERTされない...
一応、ファイルの構成は以下のようになってます。
今回、関係ありそうな部分を記載。EntityやRepository、XMLについては、MyBatis Generatorで作成したものを利用してるので、割愛。
■/mybatis-example/src/main/resources/userdata.csv
first_name,last_name,rome_first_name,rome_last_name,ohenro_ids "鈴木","二郎","suzuki","jiro","1,2,3,4,5,6" "鈴木","三郎","suzuki",saburo","24,25,26,27,28" "鈴木","四郎","suzuki","shiro","40,41,42,43,44,45,46" "鈴木","五郎","suzuki","goro","81,82,83,84,85,86" "鈴木","六郎","suzuki","rokuro","1,2,3,4,5,6" "鈴木","七郎","suzuki","shichiro","24,25,26,27,28,29" "鈴木","八郎","suzuki","hachiro","40,41,42,43,44,45,46,47" "鈴木","九郎","suzuki","kyuro","81,82,83,84,85,86" "鈴木","十郎","suzuki","jyuro","1,2,3,4,5,6"
■/mybatis-example/src/main/java/com/example/demo/dto/CsvUser.java
package com.example.demo.dto; import lombok.Builder; import lombok.Data; @Data @Builder public class CsvUser { private String last_name; private String first_name; private String rome_last_name; private String rome_first_name; private String ohenro_ids; }
■/mybatis-example/src/main/java/com/example/demo/batch/user/UserReader.java
package com.example.demo.batch.user; import java.io.FileReader; import java.io.IOException; import java.nio.charset.Charset; import java.util.Objects; import org.springframework.batch.item.ReaderNotOpenException; import org.springframework.batch.item.file.ResourceAwareItemReaderItemStream; import org.springframework.batch.item.file.mapping.FieldSetMapper; import org.springframework.batch.item.file.transform.DefaultFieldSet; import org.springframework.batch.item.file.transform.FieldSet; import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; import org.springframework.util.Assert; import org.springframework.validation.BindException; import com.opencsv.CSVParserBuilder; import com.opencsv.CSVReader; import com.opencsv.CSVReaderBuilder; import com.opencsv.exceptions.CsvValidationException; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @Slf4j @Setter public class UserReader<T> extends AbstractItemCountingItemStreamItemReader<T> implements ResourceAwareItemReaderItemStream<T>, InitializingBean { public static final Charset DEFAULT_CHARSET = Charset.defaultCharset(); private Resource resource; private boolean noInput = false; private Charset charset = DEFAULT_CHARSET; private int linesToSkip = 0; private boolean strict = true; private char delimiter = ','; private char quotedChar = '"'; private char escapeChar = '"'; private String[] headers; private FieldSetMapper<T> fieldSetMapper; private CSVReader csvReader; private Resource resourceToRead; @Override public void afterPropertiesSet() throws Exception { Assert.notNull(this.headers, "header is required"); Assert.notNull(this.fieldSetMapper, "FieldSetMapper is required"); } @Override public void setResource(Resource resource) { this.resourceToRead = resource; } @Override protected T doRead() { if (noInput) { return null; } if (csvReader == null) { throw new ReaderNotOpenException("CSVReader is not initialized"); } String[] line; T mapField = null; try { line = csvReader.readNext(); if (Objects.isNull(line)) { return null; } FieldSet fs = new DefaultFieldSet(line, headers); mapField = fieldSetMapper.mapFieldSet(fs); } catch (CsvValidationException | IOException | BindException e) { // log.error("error", e); } return mapField; } @Override protected void doOpen() throws Exception { //Assert.notNull(resource, "Input resource must be set"); noInput = true; if(Objects.nonNull(resource) && !resource.exists()){ if(strict) { throw new IllegalStateException("Input resource must exist (reader is in 'strict' mode): " + resource); } log.warn("Input resource does not exist " + resource.getDescription()); return; } if(Objects.nonNull(resource) && !resource.isReadable()){ if(strict){ throw new IllegalStateException("Input resource must be readable (reader is in 'strict' mode): " + resource); } log.warn("Input resource is not readable " + resource.getDescription()); } var csvParserBuilder = new CSVParserBuilder().withSeparator(delimiter) .withQuoteChar(quotedChar) .withStrictQuotes(true); if (quotedChar != escapeChar) { csvParserBuilder.withEscapeChar(escapeChar); } csvReader = new CSVReaderBuilder(new FileReader(resourceToRead.getFile(), charset)) .withCSVParser(csvParserBuilder.build()) .withSkipLines(linesToSkip) .build(); noInput = false; } @Override protected void doClose() throws Exception { if (Objects.nonNull(csvReader)) { csvReader.close(); } } }
■/mybatis-example/src/main/java/com/example/demo/batch/user/CsvUserMapper.java
package com.example.demo.batch.user; import org.springframework.batch.item.file.mapping.FieldSetMapper; import org.springframework.batch.item.file.transform.FieldSet; import org.springframework.stereotype.Component; import org.springframework.validation.BindException; import com.example.demo.dto.CsvUser; @Component public class CsvUserMapper implements FieldSetMapper<CsvUser> { @Override public CsvUser mapFieldSet(FieldSet fieldSet) throws BindException { return CsvUser.builder() .first_name(fieldSet.readString(0)) .last_name(fieldSet.readString(1)) .rome_first_name(fieldSet.readString(2)) .rome_last_name(fieldSet.readString(3)) .ohenro_ids(fieldSet.readString(4)) .build(); } }
■/mybatis-example/src/main/java/com/example/demo/batch/user/UserProcessor.java
package com.example.demo.batch.user; import org.springframework.batch.item.ItemProcessor; import org.springframework.stereotype.Component; import com.example.demo.dto.CsvUser; import com.example.demo.entity.ShikokuOhenroUser; @Component public class UserProcessor implements ItemProcessor<CsvUser, ShikokuOhenroUser> { @Override public ShikokuOhenroUser process(CsvUser item) throws Exception { // return ShikokuOhenroUser.builder() .last_name(fieldSet.readString(0)) .first_name(fieldSet.readString(1)) .rome_last_name(fieldSet.readString(2)) .rome_first_name(fieldSet.readString(3)) .ohenro_ids(fieldSet.readString(4)) .build(); } }
■/mybatis-example/src/main/java/com/example/demo/batch/user/UserWriter.java
package com.example.demo.batch.user; import java.util.List; import org.springframework.batch.item.ItemWriter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.example.demo.entity.ShikokuOhenroUser; import com.example.demo.repository.ShikokuOhenroUserMapper; @Component public class UserWriter implements ItemWriter<ShikokuOhenroUser> { @Autowired private ShikokuOhenroUserMapper shikokuOhenroUserMapper; @Override public void write(List<? extends ShikokuOhenroUser> items) throws Exception { // for (ShikokuOhenroUser user: items) { shikokuOhenroUserMapper.insert(user); } } }
■/mybatis-example/src/main/java/com/example/demo/batch/user/UserBatchConfig.java
package com.example.demo.batch.user; import java.io.IOException; import java.nio.charset.StandardCharsets; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import com.example.demo.dto.CsvUser; import com.example.demo.entity.ShikokuOhenroUser; @Configuration @EnableBatchProcessing public class UserBatchConfig { @Autowired private JobBuilderFactory jobBuilderFactory; @Autowired private StepBuilderFactory stepBuilderFactory; @Bean @StepScope public Resource userCsvResource(@Value("#{jobParameters['filePath']}") String filePath) { return new FileSystemResource(filePath); } @Bean public UserReader<CsvUser> csvUserReader(CsvUserMapper csvUserMapper) throws IOException { UserReader<CsvUser> reader = new UserReader<>(); reader.setCharset(StandardCharsets.UTF_8); reader.setStrict(true); reader.setResource(new ClassPathResource("userdata.csv")); reader.setLinesToSkip(1); reader.setHeaders(new String[] {"first_name", "last_name", "rome_first_name", "rome_last_name", "ohenro_ids"}); reader.setFieldSetMapper(csvUserMapper); reader.setDelimiter(','); reader.setQuotedChar('"'); reader.setName("csvReader"); return reader; } @Bean public Step step1(UserReader<CsvUser> userReader, UserWriter userWriter, UserProcessor userProcessor) { return stepBuilderFactory.get("csvItemReaderStep") .<CsvUser, ShikokuOhenroUser> chunk(10) .reader(userReader) .processor(userProcessor) .writer(userWriter) .build(); } @Bean public Job importUserJob(Step step1) { return jobBuilderFactory.get("importUserJob") .incrementer(new RunIdIncrementer()) .start(step1) .build(); } }
で、実行すると、CSVファイルの1つ目のデータだけINSERTされて、エラーになってしまう...
う~む、参考サイト様と同じ様に実装してるように思うのだけど、CSVファイルの1つ目のデータしか読み込めない原因が分からずです...
OpenCSVの闇なのか...
2022年11月13日(日)追記:↓ ここから
CSVライブラリの挙動の比較をしてらっしゃる方がおり、
⇧ 上記サイト様の情報によると、OpenCSVで読み取れない文字列の組み合わせがあるはあるみたいなんだけど、CSVファイルに依存してしまうということですかね...
で、CSVファイルをよく見たら、
⇧ 値にダブルクォーテーションが含まれてしまっていたので、データがダブルクォーテーションで閉じられてない箇所があったので
■/mybatis-example/src/main/resources/userdata.csv
first_name,last_name,rome_first_name,rome_last_name,ohenro_ids "鈴木","二郎","suzuki","jiro","1,2,3,4,5,6" "鈴木","三郎","suzuki",saburo","24,25,26,27,28" "鈴木","四郎","suzuki","shiro","40,41,42,43,44,45,46" "鈴木","五郎","suzuki","goro","81,82,83,84,85,86" "鈴木","六郎","suzuki","rokuro","1,2,3,4,5,6" "鈴木","七郎","suzuki","shichiro","24,25,26,27,28,29" "鈴木","八郎","suzuki","hachiro","40,41,42,43,44,45,46,47" "鈴木","九郎","suzuki","kyuro","81,82,83,84,85,86" "鈴木","十郎","suzuki","jyuro","1,2,3,4,5,6"
修正して、
■/mybatis-example/src/main/resources/userdata.csv
first_name,last_name,rome_first_name,rome_last_name,ohenro_ids "鈴木","二郎","suzuki","jiro","1,2,3,4,5,6" "鈴木","三郎","suzuki","saburo","24,25,26,27,28" "鈴木","四郎","suzuki","shiro","40,41,42,43,44,45,46" "鈴木","五郎","suzuki","goro","81,82,83,84,85,86" "鈴木","六郎","suzuki","rokuro","1,2,3,4,5,6" "鈴木","七郎","suzuki","shichiro","24,25,26,27,28,29" "鈴木","八郎","suzuki","hachiro","40,41,42,43,44,45,46,47" "鈴木","九郎","suzuki","kyuro","81,82,83,84,85,86" "鈴木","十郎","suzuki","jyuro","1,2,3,4,5,6"
アプリケーションを実行したところ、
CSVファイルの全てのデータがINSERTされました。
CSVファイルのフォーマットがしっかりしている必要があるんですね、CSVファイル扱いたくないな~...
2022年11月13日(日)追記:↑ ここまで
とりあえず、分かったことは、Spring Batchめちゃくちゃ使い辛い...
毎度モヤモヤ感が半端ない...
今回はこのへんで。