ハンドラを使用してPostgreSQLでファイル管理をする

# 概要
MyBatisのバージョンが3.4以上であれば、ハンドラクラスを作成せずとも`InputStream`を渡すだけでファイルを`byte[]`に変換してデータベースに登録することができます。
アップロードされたファイルを、MyBatisを利用してデータベースにファイルを保存したいと考えたとき、Mybatisのバージョンによっては`TypeHandler`が必要になります。
今回は`TypeHandler`を利用した方法を、覚え書きとして今回記事を書くことにしました。

## この記事のゴール
[Spring Initializr](https://start.spring.io "Spring Initializr")を利用して新規作成したプロジェクトに、`TypeHandler`を利用したファイルのアップロード機能を実装する。

## 依存関係
Spring Initializrで依存関係に追加したものは、以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/882970/28f7ae3d-8ea6-d7bd-f800-721f1b9ac1c4.png)

### 依存関係の一部

```xml:pom.xml
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-thymeleaf</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.mybatis.spring.boot</groupId>
		<artifactId>mybatis-spring-boot-starter</artifactId>
		<version>2.1.4</version>
	</dependency>
		<dependency>
		<groupId>org.postgresql</groupId>
		<artifactId>postgresql</artifactId>
		<scope>runtime</scope>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>

<build>
	<plugins>
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
		</plugin>
	</plugins>
</build>
```

## 今回作成をするものについて
作成するアプリは、以下の機能を実装していきます。

* HTMLのフォームで送信されたファイルをJavaで受け取り、そのファイルをMyBatisを利用してPostgreSQLに登録をする
* 同HTMLに登録されているファイルを一覧として表示をする
* ファイル名のリンクを押下することで、登録されているファイルをダウンロードすることができる

### ファイル階層
FileManagement
├ src/main/java
│ └ com.example.demo
│ 　 ├ controller
│ 　 │ └ FileManagementController.java
│ 　 ├ handler
│ 　 │ ├ AttachmentFileHandler.java
│ 　 │ └ InputStreamTypeHandler.java
│ 　 ├ model
│ 　 │ ├ AttachmentFile.java
│ 　 │ └ FileManagement.java
│ 　 ├ repository
│ 　 │ └ FileManagementRepository.java
│ 　 ├ service
│ 　 │ ├ FileManagementService.java
│ 　 │ └ FileManagementServiceImpl.java
│ 　 └ FileManagementApplication.java
└ src/main/resources
　 ├ com.example.demo.repository
　 │ └ FileManagementRepository.xml
　 ├ templates
　 │ └ index.html
　 └ application.yml

# データ登録の実装
まず、HTMLのフォームで送信されたファイルをJavaで受け取り、そのファイルをMyBatisを利用してPostgreSQLに登録をする機能を実装していきます。

## Viewの実装
テンプレートはThymeleafを利用しています。
ファイルをアップロードするためのフォームと、登録されたファイルの一覧をリンクで表示するだけのシンプルなものになっています。

```html:index.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="utf-8" />
	<title>ファイル管理画面</title>
</head>
<body>
	<h1>ファイル管理画面</h1>
		<h2>新規ファイル登録フォーム</h2>
		<form action="/" enctype="multipart/form-data" method="POST">
			<p>
				<input type="file" name="file" />
				<input type="submit" value="登録" />
			</p>
		</form>
		<h2>ファイル一覧</h2>
		<th:block th:if="${#lists.isEmpty(files)}">
			<p>登録されているファイルはありません</p>
		</th:block>
		<th:block  th:if="!${#lists.isEmpty(files)}">
			<tr th:each="file : ${files}">
				<td>
					<a th:text="${file.fileName}" th:href="@{'/' + ${file.id}}"></a>
				</td>
			</tr>
		</th:block>
</body>
</html>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/882970/71f6ba0f-312b-fe14-3edb-5d324642087a.png)

## PostgreSQLの定義
使用するRDBMSは、PostgreSQLです。
今回はmydbという名前のデータベースを作成し、スキーマは作成せずpublicにテーブルを作成します。
このとき、プロジェクト内の`application.yml`の定義は以下のとおりです。

```yml:application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver
```

テーブルとして定義した際に実行したSQLは以下のようになります。

```sql
CREATE SEQUENCE id_seq;

CREATE TABLE files(
id integer DEFAULT nextval('id_seq'),
file_name text,
file bytea,
CONSTRAINT pk_files PRIMARY KEY(id)
);
```

## ハンドラとモデルの実装
まず、ファイルを登録する際に利用するモデルから定義をしていきます。
ファイルの`InputStream`と、ファイルサイズを持ちます。

```java:AttachmentFile.java
public class AttachmentFile {
	private long length;
	private InputStream inputStream;

	public AttachmentFile(long length, InputStream inputStream) {
		this.length = length;
		this.inputStream = inputStream;
	}

	public long getLength() {
		return length;
	}
	public InputStream getInputStream() {
		return inputStream;
	}
}
```

次に上記モデルクラスを使った`TypeHandler`の定義です。
ハンドラクラスは、`org.apache.ibatis.type.BaseTypeHandler`を継承する必要があり、パラメータがNULLだった場合などの動作を定義します。

```java:AttachmentFileTypeHandler.java
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import com.example.demo.model.AttachmentFile;

public class AttachmentFileTypeHandler extends BaseTypeHandler<AttachmentFile> {

	@Override
	public void setNonNullParameter(PreparedStatement ps, int i, AttachmentFile parameter, JdbcType jdbcType)
			throws SQLException {
		ps.setBinaryStream(i, parameter.getInputStream(), parameter.getLength());
	}

	@Override
	public AttachmentFile getNullableResult(ResultSet rs, String columnName) throws SQLException {
		return toInputStream(rs.getBlob(columnName));
	}

	@Override
	public AttachmentFile getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
		return toInputStream(rs.getBlob(columnIndex));
	}

	@Override
	public AttachmentFile getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
		return toInputStream(cs.getBlob(columnIndex));
	}

	private AttachmentFile toInputStream(Blob blob) throws SQLException {
		if (blob == null) {
			return null;
		} else {
			return new AttachmentFile(-1, blob.getBinaryStream());
		}
	}
}
```

次にファイルの受け取りに関するモデルとハンドラクラスを定義します。
これらは、ファイルの一覧としてHTMLにファイル名を列挙するために使用したり、ファイルのダウンロードに使用されます。

```java:FileManagement.java
public class FileManagement {
	private int id;
	private String fileName;
	private InputStream fileData;

	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getFileName() {
		return fileName;
	}
	public void setFileName(String fileName) {
		this.fileName = fileName;
	}
	public InputStream getFileData() {
		return fileData;
	}
	public void setFileData(InputStream fileData) {
		this.fileData = fileData;
	}
}
```

受け取りのハンドラクラスも、`org.apache.ibatis.type.BaseTypeHandler`を継承します。
受け取りは`InputStream`で行うので、ファイル登録と同じように`InputStream`がNULLの場合などの動作を定義します。

```java:InputStreamTypeHandler.java
import java.io.InputStream;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

public class InputStreamTypeHandler extends BaseTypeHandler<InputStream> {

	@Override
	public void setNonNullParameter(PreparedStatement ps, int i, InputStream parameter, JdbcType jdbcType)
			throws SQLException {
		ps.setBlob(i, parameter);
	}

	@Override
	public InputStream getNullableResult(ResultSet rs, String columnName) throws SQLException {
		return rs.getBinaryStream(columnName);
	}

	@Override
	public InputStream getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
		return rs.getBinaryStream(columnIndex);
	}

	@Override
	public InputStream getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
		return cs.getResultSet().getBinaryStream(columnIndex);
	}
}
```

### BLOBについて
`import java.sql.Blob`としているように、ファイル管理にはBLOB（Binary Large OBject）を利用します。
BLOBとは、バイナリデータをデータベースに登録する際に使われるデータ型です。
MyBatisは、`InputStream`で渡されたデータをバイナリデータとしてデータベースに保存します。
あまり意識せずとも利用ができますが、**約1GBまでのファイルサイズしか扱うことが出来ない**ことには注意しなければなりません。

## RepositoryとTypeHandlerの実装
リポジトリと、MybBatisのxml定義は以下のようになります。

```java:FileManagementRepository.java
@Mapper
public interface FileManagementRepository {
	List<FileManagement> findAll();
	void insertFile(@Param("fileName") String fileName, @Param("attachmentFile") AttachmentFile attachmentFile);
	FileManagement getFileDataById(@Param("id") int id);
}
```

```xml:FileManagementRepository.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.example.demo.repository.FileManagementRepository">
  <resultMap id="fileManagement" type="com.example.demo.model.FileManagement">
    <result column="id" property="id" />
    <result column="file_name" property="fileName" />
    <result column="file" property="fileData" typeHandler="com.example.demo.handler.InputStreamTypeHandler" />
  </resultMap>

  <select id="findAll" resultMap="fileManagement">
    SELECT
      id, file_name
    FROM
      files
    ORDER BY
      id ASC
  </select>

  <insert id="insertFile">
    INSERT INTO
      files
    (
      file_name,
      file
    )
    VALUES
    (
      #{fileName},
      #{attachmentFile, typeHandler=com.example.demo.handler.AttachmentFileTypeHandler}
    )
  </insert>

  <select id="getFileDataById" parameterType="int" resultMap="fileManagement">
    SELECT
      file_name,
      file
    FROM
      files
    WHERE
      id = #{id}
  </select>
</mapper>
```

それぞれの動作は

* ファイル名の一覧を取得する
* ファイルを登録する
* ファイルのデータを取得する

となっており、データの取得には`FileManagement`クラスを使用しています。
重要なのが`AttachmentFileTypeHandler`と`InputStreamTypeHandler`を指定している部分で、それぞれ登録と受け取りの際に`typeHandler`で指定をしています。
このようにする事で、PostgreSQLにBLOBとしてデータを登録することと、登録されたデータを`InputStream`として受け取ることが出来ます。
また、テーブルの主キーに設定している`id`については、テーブルの定義の際に`id integer default nextval('id_seq')`としているので、データの登録時に`id`を指定をしない事でデフォルト値を使用するようにしています。

## Serviceの実装
ここまで実装してきたものを利用するサービスクラスは、以下のようになります。

```java:FileManagementService.java
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.model.FileManagement;

public interface FileManagementService {
	public List<FileManagement> findAll();
	public void insertFile(MultipartFile uploadFile);
	public FileManagement getFileData(int id);
}
```

```java:FileManagementServiceImpl.java
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

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

import com.example.demo.model.AttachmentFile;
import com.example.demo.model.FileManagement;
import com.example.demo.repository.FileManagementRepository;

@Service
public class FileManagementServiceImpl implements FileManagementService {
	@Autowired
	FileManagementRepository reposiroty;

	public List<FileManagement> findAll() {
		return reposiroty.findAll();
	}

	public void insertFile(MultipartFile uploadFile) {
		String fileName = uploadFile.getOriginalFilename();
		InputStream inputstream = null;
		AttachmentFile attachmentFile = null;
		try {
			inputstream = uploadFile.getInputStream();
			attachmentFile = new AttachmentFile(uploadFile.getSize(), inputstream);
			reposiroty.insertFile(fileName, attachmentFile);
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (inputstream != null) {
				try {
					inputstream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	public FileManagement getFileData(int id) {
		return reposiroty.getFileDataById(id);
	}
}
```

ここまで実装した機能を利用する為の処理をしています。
アップロードされたファイルは、コントローラクラスで`org.springframework.web.multipart.MultipartFile`として受け取るので、そこからファイル名やファイルサイズ、ファイルの実態を取得しています。

## Controllerの実装
最後に、今まで実装してきた機能のコントローラクラスを作成します。
作成したクラスは以下のようになります。

```java:FileManagementController.java
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import com.example.demo.model.FileManagement;
import com.example.demo.service.FileManagementService;

@Controller
public class FileManagementController {

	@Autowired
	FileManagementService service;

	@RequestMapping(value = "/", method = RequestMethod.GET)
	public String showIndexPage(Model model) {
		List<FileManagement> files = service.findAll();
		model.addAttribute("files", files);
		return "index";
	}

	@RequestMapping(value = "/", method = RequestMethod.POST)
	public String postFile(@RequestParam("file") MultipartFile uploadFile, Model model) {
		if (uploadFile.isEmpty()) {
			return showIndexPage(model);
		}
		service.insertFile(uploadFile);
		return showIndexPage(model);
	}

	@RequestMapping(value = "/{id}", method = RequestMethod.GET)
	public void download(@PathVariable("id") int id, HttpServletResponse res) throws IOException {
		FileManagement file = service.getFileData(id);
		if (file.getFileData() != null) {
			res.setContentType("application/octet-stream;charset=UTF-8");
			String fileName = file.getFileName();
			res.setHeader("Content-Disposition", "attachment;filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"");
			IOUtils.copy(file.getFileData(), res.getOutputStream());
		} else {
			res.setStatus(404);
		}
	}
}
```

それぞれ

* フォームとファイルの一覧を表示するリクエスト
* ファイルを登録するリクエスト
* ファイルをダウンロードするリクエスト

をマッピングしています

### ファイルをダウンロードするリクエストについて
少し複雑になっているのがファイルをダウンロードするリクエストについてで、まずHTMLからは`<a th:text="${file.fileName}" th:href="@{'/' + ${file.id}}"></a>`で作成されたリンクでリクエストされます。
指定された`id`のファイルデータをテーブルから取得して、`HttpServletResponse`に情報を詰めていきます。
`IOUtils.copy(file.getFileData(), res.getOutputStream())`としている部分は

```java
InputStream in = file.getFileData();
OutputStream out = response.getOutputStream();
byte[] buffer = new byte[1024 * 4];
while ((n = in.read(buffer, 0, buffer.length)) != -1) {
    out.write(buffer, 0, n);
}
```

とする処理を、便利なメソッドで省略をしています。

# 動作確認
実装したプロジェクトを動作させた際のブラウザの動作確認を、スクリーンショットで掲載します。
それぞれ

* 初めてHTMLを表示して、アップロードするファイルを選択したとき
* ファイルを選択した状態で登録ボタンを押下したとき
* 一覧のファイル名のリンクを押下して、ファイルがダウンロードされたとき

になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/882970/da605463-3934-0c9b-88bc-e91066b51f26.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/882970/08366b05-e853-f831-24e6-bcfad2a83da7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/882970/1be5db51-8013-0313-80e5-e72cf3b26021.png)

# おわりに
今回関係のないソースコードを減らすために、必要になるであろう例外のチェックを最低限しかしていません。
実際に実装をする際には、例外処理をしっかりとする必要があります。
また、今回の記事はあくまでMyBatisのバージョンが3.4未満の場合に必要になる`TypeHandler`の定義になります。
MyBatisのバージョンが古くて`TypeHandler`が必要な時には、今回のような定義をする必要があります。

# 参考記事
[MyBatis 3.4でBLOB/CLOBのTypeHandlerがサポートされた](https://qiita.com/kazuki43zoo/items/2bc5e9dd9a363066f0ca)

[Commons IO 便利なメソッド その１](https://deathma.hatenadiary.org/entry/20070317/1174139738)