【Java】ファイルをアップロードして中身をDBに登録したい

## はじめに
今回は画面からcsvファイルをアップロードし、ファイル内のデータをDBに登録する処理を作ってみます。初学者の方にとって少しでも参考になれば幸いです。

## 環境
windows11
Java 21
Spring Boot 3.4.1
Maven 3.9.9
Thymeleaf 3.1.3
こちらを参考にしていただければ同じ環境の構築が可能です。

https://qiita.com/uniuni108/items/1362a9bf21faf6b0bbd3

https://qiita.com/uniuni108/items/33a25420bd50a691465c

### アップロード
まずは、画面からファイルをアップロードし任意のフォルダに保存しておく処理を作ります。

* uploadForm.html
画面側ではファイルを選択してアップロードボタンを押すだけの簡素な作りにしています。
アップロードの結果が分かるようにmessageの値を表示します。
```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ファイルアップロード</title>
</head>
<body>
	<div><h2>ファイルアップロード</h2></div>
	
	<div>
		<!-- アップロードフォーム -->
		<form action="/upload" method="post" enctype="multipart/form-data">
			<label for="file">ファイルを選択:</label>
		    <input type="file" id="file" name="file" required>
		    <button type="submit">アップロード</button>
		</form>
	</div>
	
	<div th:if="${message}">
		<p th:text="${message}"></p>
	</div>
</body>
</html>
```

* FileUploadController.java
ファイルを任意のフォルダに格納する処理です。
あとから少し処理は変える予定ですが一旦ここまでで動作確認して問題ないことを確認します。
```java
package com.example.file.controller;

import java.io.File;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class FileUploadController {

	@GetMapping("/")
    public String showUploadForm() {
        return "uploadForm";
    }
	
	@PostMapping("/upload")
    public String fileUploadFile(@RequestParam("file") MultipartFile file, Model model) {
        
		try {
			// ファイルを保存する場所
            String uploadDir = "任意のパス";

            // 保存するファイルのパスを生成
            File dest = new File(uploadDir + file.getOriginalFilename());
            file.transferTo(dest);
			
			model.addAttribute("message", "ファイルがアップロードされました");
		} catch (Exception e) {
			model.addAttribute("message", "ファイルアップロードに失敗しました");
		}
		
		return "uploadForm";
    }
}

```

### ファイルの読み込みと登録
実際にファイルの中身を読み込み、データを登録する処理を追加していきます。

* FileUploadController.java
```java
package com.example.file.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import com.example.file.service.FileUploadService;

@Controller
public class FileUploadController {
	
	@Autowired
	FileUploadService fileUploadService;
	

	@GetMapping("/")
    public String showUploadForm() {
        return "uploadForm";
    }
	
	@PostMapping("/upload")
    public String fileUploadFile(@RequestParam("file") MultipartFile file, Model model) {
        
		try {
			String message = fileUploadService.uploadFile(file);
			
			model.addAttribute("message", message);
		} catch (Exception e) {
			logger.error(e.getMessage());
			model.addAttribute("message", "ファイルアップロードに失敗しました");
		}

		return "uploadForm";
    }
}
```

* FileUploadServiceImpl.java
```java
package com.example.file.service.impl;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.example.file.logic.CSVFileUploadLogic;
import com.example.file.service.FileUploadService;


@Service
public class FileUploadServiceImpl implements FileUploadService {
	
	@Autowired
	private CSVFileUploadLogic csvFileUploadLogic;
	
	@Override
	public String uploadFile(MultipartFile file)  throws IllegalStateException, IOException {
		
		if (!csvFileUploadLogic.validateCSVFile(file))
			return "ファイルが正しくありません";
		
		csvFileUploadLogic.regist(file);
		
		csvFileUploadLogic.upload(file);
		
		return "完了";
	}
}
```

* CSVFileUploadLogicImpl.java
実際にファイルの中身を読み込んでDBに登録する処理を実装しました。
アップロードするメソッドと分けています。
ファイルのバリデート処理はダミーです。
```java
package com.example.file.logic.impl;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import com.example.file.logic.CSVFileUploadLogic;
import com.example.file.mapper.TestMapper;
import com.example.file.model.Test;

@Component
public class CSVFileUploadLogicImpl implements CSVFileUploadLogic {
	
	@Autowired
	private TestMapper testMapper;
	
	@Override
	public boolean validateCSVFile(MultipartFile file) {
		return true;
	}
	
	@Override
	public void regist(MultipartFile file) throws IOException {
		
		InputStreamReader inputStream = new InputStreamReader(file.getInputStream());
		BufferedReader bufferedReader = new BufferedReader(inputStream);
		
		String line;

        // ヘッダー行は登録対象外
		boolean firstLine = true;
        // 登録するデータ格納用リスト
		List<Test> testList = new ArrayList<>();

        // 1行ずつ読み込み
		while ((line = bufferedReader.readLine()) != null) {
			if (firstLine) {
                firstLine = false;
                continue;
            }
			
			String[] colData = line.split(",");
			
			Test test = new Test();
			test.setComment(colData[0]);
			test.setRegistUser(colData[1]);
			testList.add(test);
		}
		// Mapper呼び出し
		testMapper.regist(testList);
	}
	
	@Override
	public void upload(MultipartFile file) throws IllegalStateException, IOException {
		// ファイルを保存する場所
        String uploadDir = "任意のパス";

        // 保存するファイルのパスを生成
        File dest = new File(uploadDir + file.getOriginalFilename());
        file.transferTo(dest);
	}
}


```

* Test.java
Dataアノテーションを利用することでgetter/setterを自動生成してくれます。

```java
package com.example.file.model;

import lombok.Data;

@Data
public class Test {
	
	private int id;
    private String comment;
    private String registUser;
}
```

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

<mapper namespace="com.example.file.mapper.TestMapper">
    <insert id="regist">
        INSERT INTO TestTable(comment, registuser) VALUES
        <foreach collection="list" item="test" index="index" separator=",">
            (#{test.comment}, #{test.registUser})
        </foreach>
    </insert>
</mapper>
```

### 動作確認
画面からアップロードしてみます。
カンマ区切りのcsvファイルを作りました。
![動作確認4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967914/4aa41ae9-4b78-7656-10ce-612172c68170.png)

画面を開いてファイルを選択した状態がこちらです。
![動作確認2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967914/2be214bb-52da-1a3e-8811-9f333b193780.png)

アップロードボタンを押下すると「完了」というメッセージが表示されました。
![動作確認3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967914/fd852877-dfcb-2411-dbe6-16bd61f883f0.png)

事前の確認の影響で、idが途中から採番されていますがデータの登録ができています。
![動作確認5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967914/a2c0620c-357b-a6d4-e023-f2bf86528b8d.png)

## あとがき
最後まで読んでいただきありがとうございます。

今回はファイルの保存とデータを登録する処理だけでしたので、目を瞑ったところだけ書き残しておきます。
* パスはプロパティファイルなどで管理
* アップロードするファイルのエラーチェック
    拡張子、ファイルサイズ、ヘッダー行の有無、項目数、桁数など
* コントローラに丸投げしている例外