Spring Boot で作る Web アプリケーション〜DBアクセス編〜

# はじめに

Spring Boot + Thymeleaf を使用して，インテグレーションしながら Web アプリケーションを作りたいと思います．

前回「[Spring Boot で作る Web アプリケーション〜Hello World 編〜
](https://qiita.com/rcftdbeu/items/0be265f81c4e93f0dd1e)」の続きです．

今回は DB へアクセス(初期化，登録，参照)する機能を実装したいと思います．
本投稿ではDBから値の取得/登録できる以下のような画面を表示することがゴールです．

<img width="734" alt="スクリーンショット 2020-06-21 14.49.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111511/9e5bb3af-7680-8523-4fd7-1097e1342357.png">


## 今回の材料

- Spring Boot
- Flayway
- Mybatis
- h2database

完成したソースは[こちら](https://github.com/HayashiTakaki/SpringBootNeko/releases/tag/1.2)から参照可能です．

## 概要

今回使う材料の概要です．

### Flayway

DB のマイグレーションツフレームワークです．
雑に説明すると，自動で DB のテーブル作成やバージョンアップを自動でやってくれるツールです．

メリット

- 環境構築時に手動で SQL を流さなくても良い
- 現在の DB のバージョンを気にせずに起動すれば自動的に最新になる
  → アプリを起動したけど SQL エラーが発生した．原因を調査したら DB のテーブルが古かったということがなくなる

### Mybatis

SQL と Java オブジェクトを紐付ける永続化フレームワークです．
雑に説明すると XML に記載した SQL と Java のコードを対応付けて，自動で型変換してくれたりします．
Java の String 型の連結で SQL を書くような SQL と決別できます．

### h2database

Java プラットフォーム上で動く、ACID 関係データベースです．
Oracle や PostgreSQL でも良いのですが，テスト用ということで軽くて環境構築の手間がかからないためこれを使います．
インメモリ DB ですので軽量ですが，永続化しないと終了と同時にデータが全て蒸発します．

# インストール

今回使う３つを依存関係に追加するために，build.gradle の dependencies に以下を追記します．

```properties:build.gradle
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	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') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
	implementation 'org.flywaydb:flyway-core' // 追加
 	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3' // 追加
 	runtimeOnly 'com.h2database:h2' // 追加
}
```

これだけでインストールは完了です．

# DB の初期設定

まずは DB の定義ファイルを作成していきます．
デフォルトでは`src/main/resources/db/migration`に設定されているので，存在しないフォルダは作成しつつ SQL ファイルを作成していきましょう．
ファイル名の書式は以下となっています．
`V{バージョン番号}__{説明}.sql`
※　区切りはアンダースコアが２つ

ファイル名の例

- V1.0\_\_init.sql
- V1.0.1\_\_insert_test_data.sql

バージョンは数字の小さいものから順に実行されていきます．
今回は`V1.0__init.sql`としてテーブルの定義と初期データを投入します．

```sql:src/main/resources/db/migration/V1.0__init.sql
CREATE TABLE neko_table (
    id INT,
    name VARCHAR(30),
    age INT
);

insert into neko_table (id, name, age) values (1, 'たま', 3);
insert into neko_table (id, name, age) values (2, 'みけ', 1);
insert into neko_table (id, name, age) values (3, 'くろ', 4);
```

これだけで，アプリ起動時に DB の現在のバージョンを参照し，空の DB であれば作成し，最新でなければ最新の定義に更新してくれます．

# DB から情報を取得する

DB からの値の取得，登録は Mybatis を利用します．

## domain

DB のカラムと一致する名前で作成すると楽なので，メンバ変数を DB のカラムと同じ名称にしたエンティティを作成します．

```java:Neko.java
package com.example.demo.domain;
import lombok.Data;
@Data // Lombokでgetterやsetterを自動生成
public class Neko {
	private int id;
	private String name;
	private int age;
}
```

Lombok で getter/setter を自動生成するためコード量を削減できます．

## mapper

### インターフェース

DB アクセスのインターフェースを定義します．

```java:NekoMapper.java
package com.example.demo.mapper;
import java.util.ArrayList;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import com.example.demo.domain.Neko;
@Repository // springのDIの対象となる
@Mapper // MybatisでxmlのSQLが対応づけられる
public interface NekoMapper {
	public ArrayList<Neko> getNekoList();
	public boolean insertNeko(Neko neko);
}
```

雑に全件取得，追加の 2 メソッドを準備しておきます．
`@Repository`アノテーションにより Spring の DI に登録され，
`@Mapper`アノテーションを付与したインターフェースに xml で定義した SQL が対応づけられます．
該当のメソッドを実行した時の SQL は XML で定義します．
`@Sql`アノテーションでも記載できますが，アノテーションだけでは実現できないことがある点と，複数行になると可読性が低下する点から XML ファイルでの定義とします．

### config

DB の接続設定を application.properties に追加します．

```properties:application.properties
# FlywayのDB定義
spring.flyway.url=jdbc:h2:mem:NekoDB
spring.flyway.user=user
spring.flyway.password=password

# MybatisのDB定義
spring.datasource.url=jdbc:h2:mem:NekoDB
spring.datasource.username=user
spring.datasource.password=password
```

Flyway と Mybatis それぞれで設定が必要です．
DB の URL は`jdbc:h2:mem:<DB名>`とします．
user, password はなんでも OK です．

さらに SQL を記載する xml の記載を少し楽にするために，application.properties に以下を記載します．

```properties:application.properties
# Mybatisのタイプエイリアス
mybatis.type-aliases-package=com.example.demo.domain,com.example.demo.mapper
# DB上のカラム「NEKO_TYPE」をJavaの「nekoType」に自動変換する
mybatis.configuration.map-underscore-to-camel-case=true
```

タイプエイリアスを指定することで FQCN を記載する手間を省けます．
DB ではスネークケース，Java ではキャメルケースでかかれることが多いので，自動変換の設定を有効にします．
※　今回のコードでは不要です．

### SQL

続いて，SQL の定義です．
Mapper と同じファイル名で作成する必要があるので，以下の階層に保存します．
`/src/main/resources/com/example/demo/mapper/NekoMapper.xml`

```xml:/src/main/resources/com/example/demo/mapper/NekoMapper.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のFQCNを指定 -->
<mapper namespace="com.example.demo.mapper.NekoMapper">
    <!-- select文はselectタグを利用する．idはインターフェースのメソッド名とする．resultTypeで結果を格納するJavaのオブジェクトを指定 -->
    <select id="getNekoList" resultType="Neko">
        SELECT * FROM neko_table
    </select>
    <!-- insert文はinsertタグを利用する．idはインターフェースのメソッド名とする．-->
    <!-- parameterTypeで引数のJavaのオブジェクトを指定．#{name}と指定すると，オブジェクトのgetName()から値を取得する． -->
    <insert id="insertNeko" parameterType="Neko">
        INSERT INTO neko_table (id, name, age) VALUES (#{id}, #{name}, #{age})
    </insert>
</mapper>
```

config でタイプエイリアスを指定しているので，resultType は FQCN でなくクラス名を記載するだけで OK です．

## Service

```java:NekoService.java
package com.example.demo.service;
import java.util.ArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.domain.Neko;
import com.example.demo.mapper.NekoMapper;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class NekoService {

	@Autowired
	private final NekoMapper mapper;

	/**
	 * ねこの一覧を取得する
	 *
	 * @return 一覧
	 */
	public ArrayList<Neko> getNekoList() {
		return mapper.getNekoList();
	}

	/**
	 * ねこを登録する
	 *
	 * @param neko
	 * @return
	 */
	public boolean insertNeko(Neko neko) {
		return mapper.insertNeko(neko);
	}
}
```

前回は，配列を作成して return していましたが，今回は DB から情報を取得するため，mapper を実行します．
※　エラーチェックなどが何も入っていないため，ほとんど仕事してません．

### Contorller

前回から変更はありません．

### view

前回から変更はありません．

### 動作確認

http://localhost:8080/neko
にアクセスして，以下の画面が表示されれば OK です．
<img width="557" alt="スクリーンショット 2020-06-21 14.24.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111511/d39167bf-0855-a93a-a26f-e2769cb18ca7.png">

テーブルの中身は，Flyway で投入した初期データになっていると思います．

# DB へのデータ登録

データ参照の時に半分くらい準備してましたが，データ登録(insert)も実装したいと思います．

### view

データ登録用の Form を定義します．

```html:neko.html
<form th:action="@{neko}" th:method="POST">
  <div>登録フォーム</div>
  <input type="text" name="id" placeholder="ID" />
  <input type="text" name="name" placeholder="名前" />
  <input type="text" name="age" placeholder="年齢" />
  <input type="submit" value="登録" />
</form>
```

POST 先は GET と同じく neko としておきます．

### Controller

PSOTに対応させるためにNekoController.javaに以下のメソッドを追加します．

```java:NekoController.java
	@PostMapping("neko") // nekoへのPOSTを制御する
	public String insertNeko(@ModelAttribute Neko form, Model model) { // formとしてNekoをそのまま使う(手抜き)
		service.insertNeko(form); // DBにinsert
		model.addAttribute("nekoList", service.getNekoList()); // serviceから一覧を再取得
		return "neko"; // neko.htmlをクライアントに返す
	}
```

Formは新規に作成すべきなのですが，手抜きでNekoオブジェクトをそのまま使ってます．
渡されたFormをそのままserviceのinsertnekoを使ってDBに登録します．

### 動作確認

http://localhost:8080/neko
にアクセスすると雑なFormが増えています．
<img width="733" alt="スクリーンショット 2020-06-21 14.48.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111511/f83818d7-5810-be02-bd5c-30d862a18436.png">

ここに適当に値を入力して「登録」ボタンをクリックすることで，データが追加されます．
<img width="734" alt="スクリーンショット 2020-06-21 14.49.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111511/9e5bb3af-7680-8523-4fd7-1097e1342357.png">


# まとめ

SpringBoot + Flyway + Mybatis を使って DB アクセスが実現できました．
次回はフォームのバリデーションを実装したいと思います．