Spring Batchでcsvを取り込むバッチを作ってみました。

#はじめに

おはようございます。先日Springを入門し、SpringBatchで小さいバッチを作ってみました。
皆にお役に立つかと思いますので、共有させていただきます。m(_ _)m

日本語を勉強中ですので間違っていることがあればよろしくお願いします。

それでは始めます。

#1.要件定義
csv形式で保存される販売情報をシステムに取り組むバッチを作ります。

実際、ウェブアプリでもいけますが（例えばサイトの画面でファイルを選んで、送信するとcsvを取り組むアプリなど）、SpringBatchを使ってみたいので、この場面バッチを作る必要があるとします。

それではプログラミングのInput、outputから説明してきます。

##1.1.（インプット）販売情報のcsvの内容
orders.csv

```
customer_id,item_id,item_name,item_price,purchase_date
1,1,150000,asus notebook,2020/07/16 8:00:00
1,2,200000,macbook pro 13 inch,2020/07/16 9:00:00
2,2,200000,macbook pro 13 inch,2020/07/16 10:00:00
2,3,250000,macbook pro 15 inch,2020/07/17 10:00:00
```

ファイルの内容はいつ、どのユーザーがどの商品、いくらで購入したかの情報です。

例えば2行目は、customerの1がitemの1のasus notebookを2020/07/16に15万円で購入したという意味を示します。

##1.2.（アウトプット）テーブルの内容
上のcsvをバッチの実行で、インポートした後、該当するテーブルは下記のようになると期待します。

```
| id | customer_id | item_id | item_name | item_price | purchase_date |
|--:|--:|--:|:--|--:|:--|
| 1 | 1 | 1 | asus notebook | 150000 | 2020/07/16 |
| 2 | 1 | 2 | macbook pro 13 inch | 200000 | 2020/07/16 |
| 3 | 2 | 2 | macbook pro 13 inch | 200000 | 2020/07/17 |
| 4 | 2 | 3 | macbook pro 15 inch | 250000 | 2020/07/17 |
```

#2.準備
自分のホストPCの情報は下記です。ご参考までと思います。

+ OS : MacOS (Macbook Pro)
+ Java

```
java version "12.0.2" 2019-07-16
Java(TM) SE Runtime Environment (build 12.0.2+10)
Java HotSpot(TM) 64-Bit Server VM (build 12.0.2+10, mixed mode, sharing)
```
+ IDE 
Spring Tool 4 For Eclipse https://spring.io/tools

IDEについて自分は通常VSCodeを使っていたので、最初はVSCodeでコーディングしようと思いましたが、SpringBootパッケージをインストールしてもjavaオートインポート機能がうまくいけませんでした。。。そのためこの場合VSCodeをやめました。

またIntellJも試しましたが、CommunityバージョンはSpringをサポートしなませんね・・・

という経由で、IDEはEclipseにしました。

#3.実装
##3.1.初期化
Spring Initialzrのおかげで簡単にボイラプレートを作れました。
https://start.spring.io/

![Screen Shot 2020-07-19 at 20.18.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147307/e8e10a59-18e3-7592-28cd-b8485b789783.png)

+ プロジェクト：Maven
  + 現在はGradleのほうが流行している気がしますが、個人的な理由でMavenを選びます。
+ 言語：Java
  + 他にはできないため
+ Dependence：
  + SpringBatch：バッチを作りますので、Spring　Batchを選びます。
  + Mysql Driver：Mysqlを使いますので、そのドライバーを選びます。
  + SpringDataJPA：よくわかりませんが、MysqlDriverを操作するためのライブラリようです。

選択終わった後に、Generateを押し、「demo.zip」がダウンロードされます。

##3.2.DockerでMysqlのデータベースを作成
ホストにはすでにデータベースが使える状態であればこのステップをスキップしてもいいです。

###3.2.1.Docker-composeで構築します
場所：

```
demo/
    ...
    docker-compose.yml
    db/
    ...
```

`demo/docker-compose.yml`

```
version: '3'

services:
  database:
    image: mysql:8.0
    volumes:
      - ./db/dbdata:/var/lib/mysql
    command: ['--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci','--default-authentication-plugin=mysql_native_password']
    environment:
      MYSQL_DATABASE: demo
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: demo
      MYSQL_PASSWORD: demo
    ports:
        - "33061:3306"
```

ご覧になる通りにデーターベースの情報は：

+ 初期で作られるデータベース名：demo
+ ユーザー名：demo
+ パスワード：demo

###3.2.2.テーブルを作成
これでデータベースが作成できましたが、まだテーブルがないのでバッチから呼び出すとエラーが出ます。そのためテーブルを作ります。

データーベースを起動：

```
$ docker-compose up -d
$ mysql -u demo -P 33061 -p
```

ordersというテーブルを作ります

```
CREATE TABLE orders (
  id INT AUTO_INCREMENT PRIMARY KEY,
  customer_id INT(12) NOT NULL,
  item_id INT(12) NOT NULL,
  item_name VARCHAR(50),
  item_price INT(12) NOT NULL,
  purchase_date DATETIME
);

Query OK, 0 rows affected, 3 warnings (0.05 sec)
```

これでデータベースは大丈夫でした。

##3.3.プロジェクトをインポート
3.1からzipが作られました。次はeclipseで解凍したディレクトリをインポートします。

<img width="514" alt="Screen Shot 2020-07-19 at 20.10.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147307/407ea4dc-6c2a-2cd4-c44a-921062e6f929.png">

Import > Maven > Existing Maven Project

次はバッチを設計します。
##3.4.設計

![sample_spring_batch (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147307/51d26ca9-bb93-9fa2-c69d-23dee88e76ed.jpeg)

エントリーポイントから説明していきます。

+ **BatchProcessingApplication**：アプリのエントリーポイントです。@SpringBootApplicationのAnnotationをつけたことで、自動で同じ階増にあるクラスを読み込むようフレームワークに指定することができます。
+ **BatchConfiguration**：名前の通りに、バッチの設定はここです。
+ **(Job)importOrderJob**：バッチにあるJobの一つです。実際のバッチには複数のJobがあるのは一般ですが、今回のプログラムは簡単ですので、このJobしか存在しません。csvを取り込む役をするJobです。
+ **(Step)step1()**：importOrderJobの最初、一つのみの実行ステップです。
+ **(ItemReader) reader()** :step1のステップ内にあるもっと小さいな単位の最初のプロセスです。csvを取り込む役をします。
+ **(ItemProcessor) processor()** : ステップ内に、csvを取り込んだ後に、そのデータを処理する部分です。
+ **(ItemWriter) writer()** : csvのデータがインポート、処理された後に、それをDBに追加する部分です。
+ **(Repository) OrderRepository** : データを保存するのをサポートするSpringが用意したクラスを継承するクラスです。
+ **(Entity) Order** : csvの行をモデル化するクラスです。それとDBのテーブルを表現するクラスです。実際この2つの定義は違うクラスに分けたほうがいいですが、今回はcsvの行とテーブルの構成はほぼ一緒ですので、同じクラスにします。

##3.5.プログラミング
###3.5.1.Entityを作成
設計は上から下まで考えていましたが、コーディングはその逆です。EntityのOrderから始めます。

このクラスはcsvの構成、テーブルの構成を表現しますので、このようになります。

com.example.demo.batchprocessing.Order.java

```
package com.example.demo.batchprocessing;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="orders")
public class Order {
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private Integer id;

	private Integer customerId;

	private Integer itemId;
	
	private String itemName;

	private Integer itemPrice;
	
	private String purchaseDate;

    ....
    // Setter + Getter　
    ...
}
```

Mysqlのドライバーが認識できるように、@EntityのAnnotationをつけます。

実際テーブルとクラス名は異なりますので、フレームワークがクラスに紐付けるテーブルが認識できるように、**@Table(name="orders")** をつけます。

##3.5.2.レポジトリを作成
SpringではDBにデータを追加するにはEntityだけではまだいけないので、その隙間を満たすレポジトリを作成します。

**com.example.demo.batchprocessing.OrderRepository**

```
package com.example.demo.batchprocessing;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderRepository extends CrudRepository<Order, Integer> {
	
}
```

中身にはあまりないです。ordersテーブルでCRUD操作をしたいので、CrudRepositoryを継承します。
Orderのエンティティを操作したい、またOrderのidは形はIntegerなので、合わせて`<Order, Integer>`を継承する構文に宣言します。

##3.5.3.BatchConfigurationを作成
**com.example.demo.batchprocessing.BatchConfiguration**

```
package com.example.demo.batchprocessing;

import ....

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {
	
	@Autowired
	public JobBuilderFactory jobBuilderFactory;
	
	@Autowired
	public StepBuilderFactory stepBuilderFactory;

	@Autowired
	private OrderRepository orderRepository;
	
	@Bean
	public ItemReader<Order> reader() {
		return new FlatFileItemReaderBuilder<Order>()
			.name("orderItemReader")
			.resource(new ClassPathResource("orders.csv"))
			.delimited()
			.names(new String[] {"CustomerId", "ItemId", "ItemPrice", "ItemName", "PurchaseDate"})
			.fieldSetMapper(new BeanWrapperFieldSetMapper<Order>() {{
				setTargetType(Order.class);	
			}})
			.build();
	}
	
	@Bean
	public ItemProcessor<Order, Order> processor() {
		return new ItemProcessor<Order, Order>() {
			
			@Override
			public Order process(final Order order) throws Exception {
				return order;
			}
		};
	}
	

	@Bean
	public ItemWriter<Order> writer() {
		RepositoryItemWriter<Order> writer = new RepositoryItemWriter<>(); 
		writer.setRepository(orderRepository);
		writer.setMethodName("save");
		return writer;
	}
```

+ **ItemReader<Order> reader()** : 普段DBから取り込む、ファイルから取り込む、取り込むために複数の既存クラスの使用の選択がありますが、今回はcsvから取り込むなので、それに当てはまるクラスの**FlatFileItemReader**を選びます。
    + csvの場所は「orders.csv」であることも指定する
    + カラム名を**names()**関数で指定する
    + 読み込んだcsvの行をOrderクラスに格納するように指定する
+ **ItemProcessor<Order> processor()** : csvから読み込んだデータを処理します。今回自分は何も処理せずそのままwirterに引き渡しますが、実際はバリデーション、変換などの処理が書かれることがあるかもしれませんね。

+ **ItemWriter<Order> writer()** : processorから処理終わったデータを受け取って、DBに取り込むように処理する部分です。普段他のファイルにデータを追加、ストリームでデータを送る、などのWriterがあるかもしれませんが、この場合DBにデータを追加したいので、その該当する業務を実現するクラスの**RespositoryItemWriter**を使います。

次はJobとステップの処理を作成します。

```
	@Bean
	public Job importOrderJob()
	{
		return jobBuilderFactory.get("importOrderJob")
			.incrementer(new RunIdIncrementer())
			.listener(new JobCompletionNotificationListener())
			.flow(step1())
			.end()
			.build();
	}
	
	@Bean
	public Step step1() {
	  return stepBuilderFactory.get("step1")
	    .<Order, Order> chunk(10)
	    .reader(reader())
	    .processor(processor())
	    .writer(writer())
	    .build();
	}
```

+ 下記の設定でimportOrderJobを作成します
    + Jobが実行完了というイベントを常に聞く、あればアクションするというListenerを設定：**listener(new JobCompletionNotificationListener())**（このクラスは後で説明します）
    + step1の実行を呼び出す
+ ステップのstep1を作成します
    + 一回に10個を処理するというチャンクを宣言する
    + reader()、processor()、writer()が順番に実行するようにそれぞれ宣言する

###3.5.4.JobCompletionNotificationListenerを作成
Job実行完了のイベントが起きる時に起動されるアクションを設定します。

**com.example.demo.batchprocessing.JobCompletionNotificationListener**

```
package com.example.demo.batchprocessing;

import ....

@Component
public class JobCompletionNotificationListener extends JobExecutionListenerSupport {
	
	private static final Logger log = LoggerFactory.getLogger(JobCompletionNotificationListener.class);

	@Override
	public void afterJob(JobExecution jobExecution) {
		if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
			log.info("!!! JOB FINISHED ");
		}
	}
}
```

Jobのイベントごとにアクションをトリガーするよう、その役をするJobExecutionListenerSupportクラスを継承します。

他のイベントもあるらしいけど、実行完了のイベントのみ利用したいので、afterJob関数をオーバーライドします。
ただログに出力するようにする処理があります。

###3.5.5.BatchProcessingApplicationを作成
最後に、アプリのエントリーポイントを作成します。

**com.example.demo.batchprocessing.BatchProcessingApplication**

```
package com.example.demo.batchprocessing;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BatchProcessingApplication {
	public static void main(String[] args) throws Exception {
		System.exit(SpringApplication.exit(
            SpringApplication.run(BatchProcessingApplication.class, args))
        );
	}
}
```

単にJavaのみ作成すると、複数のクラスを呼び出す、それを連携するなどの処理が必要になりますが、Springのおかげで数行だけ書いても結構です。

要は＠SpringBootApplicationをクラスにつけることです。
そのため、自動的に同じパッケージの配下にあるクラスを宣言しなくても、ロードすることができます。

```
SpringApplication.run(BatchProcessingApplication.class, args))
```

を呼び出すだけでプログラムが実行できます。

###3.5.6.要らないクラス、処理を廃棄
SpringInitializrから作成させるプロジェクトにはデフォルト処理やクラスがあります。

今回エントリーポイントを新規に作成するので、既存のエントリーポイントを削除します。

```
$ rm src/main/java/com/example/demo/DemoApplication.java
$ rm src/main/java/com/example/demo/DemoApplicationTests.java
```

また簡単にテストコードも書きます。

com.example.demo.batchprocessing.test.DemoApplicationTests.java

```
package com.example.demo.batchprocessing.test;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class BatchProcessingTests {

   @Test
   void contextLoads() {
   }

}
```

プログラミングはこれで以上です！

#4.実行
##4.1.データベースを起動
dockerで起動します

```
$ docker-compose up -d
```

##4.2.コマンドでバッチを実行する
IDE経由で実行してもいいですが、自分が好む方法のコマンドラインで実行したいと思います。

```
$ ./mvnw spring-boot:run
```

最初に実行する時にライブラリをインストールする時間がかかりますが、2回めからは早いです。

実行結果はこんなイメージです。

<img width="1420" alt="Screen Shot 2020-07-23 at 0.10.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147307/48238398-03e0-a6cb-3ec3-646ad36fe56e.png">

データベースでデータがインサートされるかを確認します。

```
$ mysql -h 0.0.0.0 -u demo -P 33061 -p demo

$ mysql> select * from orders;
+----+-------------+---------+---------------------+------------+---------------------+
| id | customer_id | item_id | item_name           | item_price | purchase_date       |
+----+-------------+---------+---------------------+------------+---------------------+
| 85 |           1 |       1 | asus notebook       |   150000 | 2020-07-16 08:00:00 |
| 86 |           1 |       2 | macbook pro 13 inch |   200000 | 2020-07-16 09:00:00 |
| 87 |           2 |       2 | macbook pro 13 inch |   200000 | 2020-07-16 10:00:00 |
| 88 |           2 |       3 | macbook pro 15 inch |   250000 | 2020-07-17 10:00:00 |
+----+-------------+---------+---------------------+------------+---------------------+
4 rows in set (0.00 sec)    
```

csvの内容通りにデータが作成されたので、バッチ作成はとりあえずOKだと思います。

#5.終わりに
SpringBatchを入門する経由を共有させていただいました。
こかからSpringBatchを学びたい方々になにかお役に立てば幸いです。

本文にまだ説明が足りないところがあれば失礼します。
https://github.com/mytv1/sample-spring-batch

プロジェクトの処理全部は上のレポをご参考になればと思います。

#6.参考
https://spring.io/guides/gs/batch-processing

https://spring.io/guides/gs/accessing-data-mysql