【Spring Batch】テーブルデータをCSVファイルへ出力する

#はじめに
taskletを使用したバッチ処理を試してみました。
テーブルの大量読み込みにも対応できるように、MyBatisのカーソルも使用しています。

##プロジェクトの作成
[Spring Initializr](https://start.spring.io/)を使用してプロジェクトを作成します。
DependenciesでBatchとLombok、MyBatis、使用するDBのJDBCドライバー（記事ではMySQLを使用）を選択してください。

##設定ファイル

```properties:application.properties
# ログレベル
logging.level.com.sample.demo=debug

# DB接続情報
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?characterEncoding=UTF-8&serverTimezone=JST&sslMode=DISABLED
spring.datasource.username=hoge
spring.datasource.password=hoge

# フェッチサイズ
#mybatis.configuration-properties.default-fetch-size=10000
mybatis.configuration-properties.fetch-size=10000

# ファイルの書込みサイズ
demo.write-size=1000

```

##ソース
DBの読込みとファイル出力を実行します。

```java:DemoTasklet.java
package com.sample.demo;

import java.util.ArrayList;
import java.util.List;

import org.apache.ibatis.cursor.Cursor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.item.ItemStreamWriter;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component()
public class DemoTasklet implements Tasklet {
	private Logger logger = LoggerFactory.getLogger(DemoTasklet.class);

	@Autowired
	private DemoRepository demoRepository;

	@Autowired
	private ItemStreamWriter<DemoDTO> writer;

	@Autowired
	private DemoContext demoContext;

	public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
		logger.debug("DemoTasklet execute 開始");

		writer.open(chunkContext.getStepContext().getStepExecution().getExecutionContext());
		try(Cursor<DemoDTO> result = demoRepository.select()) {
			List<DemoDTO> data = new ArrayList<>();
			for(DemoDTO dto: result) {
				data.add(dto);
				if (data.size() >= demoContext.getWriteSize()) {
					writer.write(data);
					data.clear();
				}
			}
			if (data.size() > 0) writer.write(data);
		}
		writer.close();

		logger.debug("DemoTasklet execute 終了");
		return RepeatStatus.FINISHED;
	}

}
```

Taskletの登録とCSVファイルの設定を行います。

```java:BatchConfig.java
package com.sample.demo;

import java.io.IOException;
import java.io.Writer;

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.launch.support.RunIdIncrementer;
import org.springframework.batch.item.file.FlatFileHeaderCallback;
import org.springframework.batch.item.file.FlatFileItemWriter;
import org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;

@Configuration
@EnableBatchProcessing
public class BatchConfig {
	@Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Autowired
    private DemoTasklet demoTasklet;

    private Resource outputResource = new FileSystemResource("output/data.csv");

    @Bean
    public DemoContext createDemoContext() {
    	return new DemoContext();
    }

	@Bean
	public FlatFileItemWriter<DemoDTO> writer()
	{
		FlatFileItemWriter<DemoDTO> writer = new FlatFileItemWriter<>();
		writer.setResource(outputResource);
		writer.setEncoding("UTF-8");
		writer.setLineSeparator("\r\n");
		writer.setAppendAllowed(false);

		writer.setHeaderCallback(new FlatFileHeaderCallback() {
			public void writeHeader(Writer arg0) throws IOException {
				arg0.append("\"ID\",\"名前\",\"メールアドレス\"");
			}
		});

		writer.setLineAggregator(new CsvLineAggregator<DemoDTO>() {
			{
				setFieldExtractor(new BeanWrapperFieldExtractor<DemoDTO>() {
					{
						setNames(new String[] { "id", "name", "mailAddress" });
					}
				});
			}
		});

		return writer;
    }

	@Bean
	public Step step1() {
		return stepBuilderFactory.get("step1").tasklet(demoTasklet).build();
	}

	@Bean
	public Job job(Step step1) {
		return jobBuilderFactory.get("job").incrementer(new RunIdIncrementer()).start(step1).build();
	}
}
```

application.propertiesの[demo.]で定義されているkey-valueが設定されます。

```java:DemoContext.java
package com.sample.demo;

import org.springframework.boot.context.properties.ConfigurationProperties;

import lombok.Data;

@ConfigurationProperties(prefix="demo")
@Data
public class DemoContext {
	private int writeSize;
}
```

springで用意されているクラスだと囲み文字の設定が出来ないので、囲み文字の設定を行う場合、自作が必要になります。

```java:CsvLineAggregator.java
package com.sample.demo;

import java.util.Arrays;

import org.springframework.batch.item.file.transform.ExtractorLineAggregator;
import org.springframework.util.StringUtils;

public class CsvLineAggregator<T> extends ExtractorLineAggregator<T> {

	private String enclose = "\"";

	private String delimiter = ",";

	public void setEnclose(String enclose) {
		this.enclose = enclose;
	}

	public void setDelimiter(String delimiter) {
		this.delimiter = delimiter;
	}

	@Override
	protected String doAggregate(Object[] fields) {
		return StringUtils.collectionToDelimitedString(Arrays.asList(fields), this.delimiter, this.enclose, this.enclose);
	}

}
```
DTO

```java:DemoDTO.java
package com.sample.demo;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Data
public class DemoDTO {
	private String id;
	private String name;
	private String mailAddress;
}
```

SQL用のインターフェース

```java:DemoRepository.java
package com.sample.demo;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.cursor.Cursor;

@Mapper
public interface DemoRepository {

	Cursor<DemoDTO> select();

}
```
SQL

```xml:DemoRepository.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.sample.demo.DemoRepository">

    <select id="select" resultType="com.sample.demo.DemoDTO" fetchSize="${fetch-size}">
      SELECT
        id,
        name,
        mail_address as mailAddress
      FROM
        users
    </select>
</mapper>

```