※当サイトの記事には、広告・プロモーションが含まれます。

JavaのDTOやEntityをCSV形式に変換したい。jackson-dataformat-csvで実現できるみたい

www.itmedia.co.jp

⇧ やること多過ぎて手が回らないってことですかね。

優秀なエンジニアの集団でもリソース不足ってのがエグいっすな...

JavaDTOやEntityをCSV形式に変換したい。jackson-dataformat-csvで実現できるみたい

JavaでEntityやDTOのフィールドをカンマ区切りにするのに、一括で置換したいなと思ってネットで情報を漁っていたら、

qiita.com

qiita.com

⇧ jackson-dataformat-csvというライブラリで実現できるらしい。

qiita.com

CSV形式の出力の際に、Beanのフィールドの並び順通りにするには、

@JsonPropertyOrder

⇧ Beanのクラスに上記のアノテーションを付与して、並び順を指定する必要があるらしい、CSVを出力するのに何故か@JsonPropertyOrderってネーミングになってますが...

ちなみに、DTOやEntityのフィールドの値を別のDTOやEntityのフィールドに設定したいとなった時に、これまた一括で何とかしたいと思うのが人情。

ネットの情報によりますと、

qiita.com

fits.hatenablog.com

⇧ 上記サイト様によりますと、

⇧ 上記のライブラリやフレームワークで、「BeanUtils.copyProperties()」というメソッドが用意されており、Bean間で値をコピーできるらしい。

Beanはと言うと、

yyyank.blogspot.com

⇧ 上記サイト様によりますと、Beanという大きな集合の中で、役割によって、

  • Form
  • DTO(Data Toransfer Object)
  • Entity

などの呼び名が変わるということですかね。

まぁ、Javaで一時的にまとまった単位のデータを保持するための入れ物としてのクラスという感じですかね。

ちなみに、CSVというと、ヘッダーの項目が表示されるものだと思い込んでいたのだけど、

cyzennt.co.jp

⇧ みたいなフォーマットもあると。

1行毎のフォーマットは全て同じと思い込んでいたのだけど、どんなCSVのフォーマットなのか、CSVファイルの出力イメージを事前に確認する必要があるということですね...

codezine.jp

CSVとはComma-Separated Valuesの略で、カンマ区切りで並べた値という意味です。

CSVファイルフォーマットの解説|CodeZine(コードジン)

⇧ カンマ区切りになっていれば、全体のフォーマットなどは自由ということみたいですね。

出力イメージなど、事前に認識共有しておくことを忘れないようにですかね。まぁ、忘れたんだけど...

実際に動作確認してみる

一応、試してみる。

Eclipseを起動し、Spring Bootのプロジェクトを作成する。

ソースコードは以下のような感じ。

■/dto-convert-to-csv/build.gradle

plugins {
	id 'java'
	id 'war'
	id 'org.springframework.boot' version '3.2.5'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    // https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-csv
    // implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.17.1'
    implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-csv'

}

tasks.named('test') {
	useJUnitPlatform()
}

■/dto-convert-to-csv/src/main/java/com/example/demo/common/DateTimeUtil.java

package com.example.demo.common;

import java.time.Clock;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import org.springframework.stereotype.Component;

@Component
public class DateTimeUtil {

	private static Clock clock = Clock.systemDefaultZone();
	
	/**
	 * 現在日時を取得する
	 * @return localDateTime 現在日時
	 */
	public static LocalDateTime createDateTimeNow() {
		LocalDateTime localDateTime = LocalDateTime.now(clock);
		return localDateTime;
	}
	
	public static String yyyMMddFormat (LocalDateTime localDateTime) {
		String yyyyMMdd = localDateTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
		return yyyyMMdd;
	}
	
}

■/dto-convert-to-csv/src/main/java/com/example/demo/dto/ExaminationInfo.java

package com.example.demo.dto;

import java.math.BigDecimal;
import java.time.LocalDateTime;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExaminationInfo {

	// 商品コード
	@NotNull
	@Min(7)
	@Max(7)
	private String productCode;
	
	// 商品名
	@NotNull
	private String productName;
	
	// 値段
	@NotNull
	private BigDecimal price;
	
	// 数量
	@NotNull
	private Integer numberOfProduct;
	
	// 取引日時
	@NotNull
	private LocalDateTime transactionDateTime;
	
	// 仕入先企業コード
	@NotNull
	private String supplierCompanyCode;
	
	// 仕入先企業名称
	@NotNull
	private String supplierCompanyName;
	
	// 仕入先国
	private String supplierCountry;

	// 手数料率
	private BigDecimal commissionRate;

}

■/dto-convert-to-csv/src/main/java/com/example/demo/dto/ExaminationInfoData.java

package com.example.demo.dto;

import java.math.BigDecimal;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

@Data
@JsonPropertyOrder({
	"fileKbn"
	,"productCode"
	,"productName"
	,"price"
	,"numberOfProduct"
})
public class ExaminationInfoData {

	// ファイル区分
	@NotNull
	@JsonProperty("fileKbn")
	private String fileKbn;
	
	// 商品コード
	@Min(7)
	@Max(7)
	@JsonProperty("productCode")
	private String productCode;
	
	// 商品名
	@JsonProperty("productName")
	private String productName;
	
	// 値段
	@JsonProperty("price")
	private BigDecimal price;

	// 数量
	@NotNull
	@JsonProperty("numberOfProduct")
	private Integer numberOfProduct;
	
}

	

■/dto-convert-to-csv/src/main/java/com/example/demo/dto/ExaminationInfoDataKey.java

package com.example.demo.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExaminationInfoDataKey {
	// 仕入先企業コード
	private String supplierCompanyCode;
	
	// 仕入先企業名称
	private String supplierCompanyName;

}

■/dto-convert-to-csv/src/main/java/com/example/demo/dto/ExaminationInfoHeader.java

package com.example.demo.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;

import jakarta.validation.constraints.NotNull;
import lombok.Data;

@Data
@JsonPropertyOrder({
	"fileKbn"
	,"createFileDate"
	,"supplierCompanyCode"
	,"supplierCompanyName"
})
public class ExaminationInfoHeader {

	// ファイル区分
	@NotNull
	@JsonProperty("fileKbn")
	private String fileKbn;
	
	// ファイル作成日
	@JsonProperty("createFileDate")
	private String createFileDate;

	// 仕入先企業コード
	@JsonProperty("supplierCompanyCode")
	private String supplierCompanyCode;
	
	// 仕入先企業名称
	@JsonProperty("supplierCompanyName")
	private String supplierCompanyName;
	
}

■/dto-convert-to-csv/src/main/java/com/example/demo/dto/ExaminationInfoTrailer.java

package com.example.demo.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;

import jakarta.validation.constraints.NotNull;
import lombok.Data;

@Data
@JsonPropertyOrder({
	"fileKbn"
	,"numberOfRecords"
})
public class ExaminationInfoTrailer {

	// ファイル区分
	@NotNull
	@JsonProperty("fileKbn")
	private String fileKbn;
	
	// レコード件数
	@JsonProperty("numberOfRecords")
	private Integer numberOfRecords;
}

■/dto-convert-to-csv/src/main/java/com/example/demo/service/CreateCsvDataService.java

package com.example.demo.service;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;

import com.example.demo.common.DateTimeUtil;
import com.example.demo.dto.ExaminationInfo;
import com.example.demo.dto.ExaminationInfoData;
import com.example.demo.dto.ExaminationInfoDataKey;
import com.example.demo.dto.ExaminationInfoHeader;
import com.example.demo.dto.ExaminationInfoTrailer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;

@Service
public class CreateCsvDataService {

	private final static String LINE_SEPARATER_FOR_CSV = "\r\n";

	private final static String COMMA = ",";

	/**
	 * CSVファイル作成に必要な情報を取得する
	 * @param examinationInfoList 審査情報
	 * @return csvDataList CSVファイル作成に必要な情報
	 */
	public List<String> createCsvDataForExmainationInfo(List<ExaminationInfo> examinationInfoList) {

		List<String> csvDataList = new ArrayList<>();
		Map<ExaminationInfoDataKey, List<ExaminationInfoData>> examinationInfoDataMap = new HashMap<>();

		for (ExaminationInfo firstExaminationInfo : examinationInfoList) {

			/** ヘッダ重複チェック */
			String headerSupplierCompanyCode = firstExaminationInfo.getSupplierCompanyCode();
			String headerSupplierCompanyName = firstExaminationInfo.getSupplierCompanyName();
			ExaminationInfoDataKey examinationInfoDataKey = new ExaminationInfoDataKey();

			examinationInfoDataKey.setSupplierCompanyCode(headerSupplierCompanyCode);
			examinationInfoDataKey.setSupplierCompanyName(headerSupplierCompanyName);

			if (isDuplicateHeader(examinationInfoDataMap, examinationInfoDataKey)) {
				// 次のヘッダ部の処理にスキップ
				continue;
			}

			// 現在日時
			LocalDateTime localDateTime = DateTimeUtil.createDateTimeNow();

			/** Beanの生成 */
			// ヘッダ部
			ExaminationInfoHeader examinationInfoHeaderBean = new ExaminationInfoHeader();
			// ファイル区分
			examinationInfoHeaderBean.setFileKbn("1");
			// ファイル作成日付(yyyyMMdd)
			examinationInfoHeaderBean.setCreateFileDate(DateTimeUtil.yyyMMddFormat(localDateTime));
			// 仕入先企業コード
			examinationInfoHeaderBean.setSupplierCompanyCode(headerSupplierCompanyCode);
			// 仕入先企業名称
			examinationInfoHeaderBean.setSupplierCompanyName(headerSupplierCompanyName);

			// ボディ部
			//int numberOfRecords = 0;
			List<ExaminationInfoData> examinationInfoDataList = new ArrayList<>();
			for (ExaminationInfo secondExaminationInfo : examinationInfoList) {

				// 仕入先企業コードがヘッダ部のものと同一の場合
				if (Objects.nonNull(headerSupplierCompanyCode)
						&& headerSupplierCompanyCode.equals(secondExaminationInfo.getSupplierCompanyCode())) {
					ExaminationInfoData examinationInfoDataBean = new ExaminationInfoData();

					//BeanUtils.copyProperties(secondExaminationInfo, examinationInfoDataBean);
					BeanUtils.copyProperties(secondExaminationInfoBean, examinationInfoDataBean, ExaminationInfoData.class);
					examinationInfoDataBean.setFileKbn("2");

					examinationInfoDataList.add(examinationInfoDataBean);
					//numberOfRecords++;
				}

			}
			//			examinationInfoDataKey.setSupplierCompanyCode(headerSupplierCompanyCode);
			//			examinationInfoDataKey.setSupplierCompanyName(headerSupplierCompanyName);
			examinationInfoDataMap.put(examinationInfoDataKey, examinationInfoDataList);

			// トレーラ部
			ExaminationInfoTrailer examinationInfoTrailerBean = new ExaminationInfoTrailer();
			examinationInfoTrailerBean.setFileKbn("9");
			examinationInfoTrailerBean.setNumberOfRecords(examinationInfoDataList.size());

			/** Bean→CSV変換 */
			StringBuilder sb = new StringBuilder();
			CsvMapper mapper = new CsvMapper();

			// ヘッダ部
			CsvSchema csvSchemaHeader = schemaForConvertBeanToCsvStr(mapper, ExaminationInfoHeader.class);
			String examinationInfoHeaderStr = null;
			try {
				examinationInfoHeaderStr = mapper.writer(csvSchemaHeader).writeValueAsString(examinationInfoHeaderBean);
				sb.append(examinationInfoHeaderStr);

			} catch (JsonProcessingException e) {
				// TODO 自動生成された catch ブロック
				e.printStackTrace();
			}

			// データ部
			CsvSchema csvSchemaData = schemaForConvertBeanToCsvStr(mapper, ExaminationInfoData.class);
			String examinationInfoDataStr = null;
			for (ExaminationInfoData examinationInfoDataBean : examinationInfoDataList) {
				try {
					examinationInfoDataStr = mapper.writer(csvSchemaData).writeValueAsString(examinationInfoDataBean);
					sb.append(examinationInfoDataStr);
					//.append(LINE_SEPARATER_FOR_CSV);

				} catch (JsonProcessingException e) {
					// TODO 自動生成された catch ブロック
					e.printStackTrace();
				}

			}

			// トレーラ部
			CsvSchema csvSchemaTrailer = schemaForConvertBeanToCsvStr(mapper, ExaminationInfoTrailer.class);
			String examinationInfoTrailerStr = null;
			try {
				examinationInfoTrailerStr = mapper.writer(csvSchemaTrailer)
						.writeValueAsString(examinationInfoTrailerBean);
				sb.append(examinationInfoTrailerStr);

			} catch (JsonProcessingException e) {
				// TODO 自動生成された catch ブロック
				e.printStackTrace();
			}

			String csvData = sb.toString();
			csvDataList.add(csvData);
		}
		return csvDataList;
	}

	private static CsvSchema schemaForConvertBeanToCsvStr(CsvMapper mapper, Class<?> clasz) {

		CsvSchema schema = mapper.schemaFor(clasz).withoutHeader();
		//CsvSchema schema = mapper.schemaFor(clasz).withHeader();

		return schema;
	}

	private static boolean isDuplicateHeader(
			Map<ExaminationInfoDataKey, List<ExaminationInfoData>> examinationInfoDataMap,
			ExaminationInfoDataKey examinationInfoDataKey) {

		if (examinationInfoDataMap.isEmpty()) {
			return false;
		}

		for (Map.Entry<ExaminationInfoDataKey, List<ExaminationInfoData>> entry : examinationInfoDataMap.entrySet()) {
			if (entry.getKey().getSupplierCompanyCode().equals(examinationInfoDataKey.getSupplierCompanyCode())
					&& entry.getKey().getSupplierCompanyName()
							.equals(examinationInfoDataKey.getSupplierCompanyName())) {
				return true;
			}
		}
		return false;
	}

}

動作確認用のソースコード

■/dto-convert-to-csv/src/test/java/com/example/demo/service/TestCreateCsvDataService.java

package com.example.demo.service;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.example.demo.dto.ExaminationInfo;

@SpringBootTest
public class TestCreateCsvDataService {

	@Autowired
	private CreateCsvDataService createCsvDataService;
	
	@Test
	public void test() {
		List<ExaminationInfo> ExaminationInfoList = new ArrayList<>(Arrays.asList(
				new ExaminationInfo("A000001", "ほげほげアメリカンプロテイン", BigDecimal.valueOf(10000), 100, LocalDateTime.now(), "0001", "ほげほげ株式会社", "アメリカ", BigDecimal.valueOf(0.05))
				,new ExaminationInfo("A000002", "ほげほげアジアンプロテイン", BigDecimal.valueOf(8000), 100, LocalDateTime.now(), "0001", "ほげほげ株式会社", "アメリカ", BigDecimal.valueOf(0.05))
				,new ExaminationInfo("A000003", "ほげほげヨーロピアンプロテイン", BigDecimal.valueOf(12000), 100, LocalDateTime.now(), "0001", "ほげほげ株式会社", "アメリカ", BigDecimal.valueOf(0.05))
				,new ExaminationInfo("A000004", "ぴよぴよアメリカンプロテイン", BigDecimal.valueOf(11000), 100, LocalDateTime.now(), "0002", "ぴよぴよ株式会社", "アメリカ", BigDecimal.valueOf(0.05))
				,new ExaminationInfo("A000004", "ぴよぴよアメリカンプロテイン", BigDecimal.valueOf(11000), 100, LocalDateTime.now(), "0002", "ぴよぴよ株式会社", "アメリカ", BigDecimal.valueOf(0.05))
				,new ExaminationInfo("A000005", "ぽよぽよアメリカンプロテイン", BigDecimal.valueOf(13000), 100, LocalDateTime.now(), "0003", "ぽよぽよ株式会社", "アメリカ", BigDecimal.valueOf(0.05))
				,new ExaminationInfo("A000006", "ぽよぽよアフリカンプロテイン", BigDecimal.valueOf(7000), 100, LocalDateTime.now(), "0003", "ぽよぽよ株式会社", "アメリカ", BigDecimal.valueOf(0.05))
				));
		
		List<String> csvDataList = createCsvDataService.createCsvDataForExmainationInfo(ExaminationInfoList);
		for (int index = 0; index < csvDataList.size(); index++) {
			System.out.println("【" +(index + 1) + "】ファイル目のデータ");
			System.out.print(csvDataList.get(index));
		}
	}


	
}

⇧ で、JUnitで実行すると、

⇧ 「仕入先企業コード」と「仕入先企業名称」毎に、CSV形式のデータが作成できました。

それにしても、ライブラリは罠が多いですな...

毎度モヤモヤ感が半端ない…

今回はこのへんで。