SpringBoot+MyBatis+MySQLの初歩

# 概要
これまでJdbcTemplateの使用経験しかなくMyBatisを使ったことがなかったので、SpringBoot + MyBatis + MySQLを使用してみた。
## 環境
+ macOS Catalina 10.15.7
+ Java11
+ SpringBoot 2.3.4
+ MySQL 8.0
+ Gradle

##やってみたこと
今回は本の情報がDBに登録されており、それを1件取得、全件表示することにします。

 + データベースからID検索し表示
 + 全件取得して一覧表示

■初回アクセス時の画面
![初期画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250449/9f53fc14-b6be-3277-0313-548a2d58dff9.png)

■1件検索(idをフォームに入力して検索を押下)
![検索結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250449/fd9f8f99-05a3-92a2-804d-e2d33fc039a9.png)

■全件表示(一覧表示を押下)
![一覧表示.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250449/780ad9f0-6bb5-4878-2f6c-c0d53653f2cc.png)



# 準備する各種クラス・設定ファイルたち

 + build.gradle（こちらの依存関係にMyBatisを追加）
 + DB関連：data.sql、schema.sql
 + BookFormクラス(入力フォームからの値を受け取る)
 + ビュー画面(Thymeleaf使用)
 + Bookクラス（DBからの値を受け取るEntityクラス）
 + BookDaoインタフェース（DBへ問合せを行うためのインタフェース）
    + BookDao.xml(ここにSQLを記載。)
 + BookServiceクラス（Daoクラスを呼び出す。）
 + BookControllerクラス(ブラウザからのリクエストに応じたビューを返す。状況に応じてserviceクラスを呼び出す。)

なぜか、BookDaoはインタフェースを実装せずともBookServiceクラスから利用できる。裏側でごにょごにょ実装されてServiceクラスから利用できるようになっているらしい。


# ディレクトリ構成
+ MyBatisはマッピングファイル(XMLファイル)にSQLを書くやり方を採用（アノテーション内にSQLを書くやり方もあり）。マッピングファイルはresorces配下のDaoと同じ階層に配置することで、Dao利用時に自動で読み込んでくれる。

```
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── demo
    │   │               ├── ServletInitializer.java
    │   │               ├── SpringTestApplication.java
    │   │               ├── controller
    │   │               │   └── BookController.java
    │   │               ├── dao
    │   │               │   └── BookDao.java
    │   │               ├── entity
    │   │               │   └── Book.java
    │   │               ├── form
    │   │               │   └── BookForm.java
    │   │               └── service
    │   │                   └── BookService.java
    │   └── resources
    │       ├── application.properties
    │       ├── com
    │       │   └── example
    │       │       └── demo
    │       │           └── dao
    │       │               └── BookDao.xml
    │       ├── data.sql
    │       ├── schema.sql
    │       ├── static
    │       │   └── css
    │       │       └── style.css
    │       └── templates
    │           └── index.html
    └── test

```
# 依存関係

```gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'mysql:mysql-connector-java'
	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'
	}
```

# DBの定義
まずはMySQLでデータベースを作っておく。今回の例でいうとlibrary。

```sql

CREATE DATABASE library;
```
下記ファイルを用意すると、SpringBoot起動の度にテストデータを用意してくれる。

```sql:schema.sql
--booktableがあれば削除
DROP TABLE IF EXISTS booktable;

--booktableがなければ新しく作成
CREATE TABLE IF NOT EXISTS booktable(
id INT AUTO_INCREMENT,
book_name VARCHAR(50) NOT NULL,
volume_num INT NOT NULL,
author_name VARCHAR(50) NOT NULL,
published_date DATE NOT NULL,
PRIMARY KEY(id)
);
```

```sql:data.sql
--本のリスト初期データ
--idカラムはオートインクリメントなので不要
INSERT INTO booktable
(book_name, volume_num,author_name,published_date)
VALUES
( 'HUNTER X HUNTER',36,'冨樫義博','2018-10-04'),
( 'ベルセルク',40,'三浦健太郎','2018-09-28'),
( 'ドリフターズ',6,'平野耕太','2018-11-30'),
( '羅生門',1,'芥川龍之介','1915-11-01')
;
```
# 設定ファイル(application.properties)
接続先に`?serverTimezone=JST`をつけないと上手くいかない。また、`mybatis.configuration.map-underscore-to-camel-case=true`により、DBのカラム名がスネークケースであってもJava側ではキャメルケースとして認識してくれる。

```configuration
### データベース接続設定
spring.datasource.url=jdbc:mysql://localhost:3306/library?serverTimezone=JST
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
### スネークケースのDBカラム名をSpringのEntity側ではキャメルケースとして対応付けてくれる。
mybatis.configuration.map-underscore-to-camel-case=true
### 初期化を行うかの指定。
spring.datasource.initialization-mode=always
```

# 各種クラス
## Formクラス
今回は画面からはidのみ受け取るのでフィールドは1個。HTML側のinputタグ内name属性とフィールド名は一致させておく。lombokの機能により、@Dataでセッターゲッターは追記不要。フィールドの型はプリミティブ型の`int`ではなく、参照型（ラッパークラス）の`Integer`にするのが良いらしい。ゼロとnullで区別がつくため。

```java:BookForm.java
package com.example.demo.form;
import lombok.Data;

@Data
public class BookForm {
	private Integer id;
}
```

## Entityクラス
データベースから取得したデータをいったん格納するオブジェクト。Formクラスにも書いたが、型は参照型が良い。

```java:Book.java
package com.example.demo.entity;
import java.time.LocalDate;
import lombok.Data;

@Data
public class Book {
	private Integer id;
	private String bookName;
	private Integer volumeNum;
	private String authorName;
	private LocalDate publishedDate;
}
```

## Daoインタフェースとマッピングファイル
MyBatisの場合は、インタフェースをつくり@MapperアノテーションをつけることでRepositoryクラスになる。1件検索のメソッドでは、単純に数値だけ渡すよりも、Entityクラス（Bookクラス）を介した方が、マッピングファイルの中で、Entityクラスの各フィールドを柔軟に参照できる。

```java:BookDao
package com.example.demo.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.example.demo.entity.Book;

@Mapper
public interface BookDao {
	//1件検索
	Book findById(Book book);

	//全件取得
	List<Book> findAll();
}
```
【XMLファイルについて】

+ ファイル名は対応するDaoと同じにする。
+ 配置場所はresorces配下のDaoインタフェースと同じ階層にする。
+ `namespace属性`にはDaoインタフェースの完全修飾クラス名を書く。
+ `select要素`にSELECT文を書く。
    + `id属性`にはDaoインタフェースの対応するメソッド名を書く。
    + `resultType属性`には検索結果をマッピングするクラス名を書く。今回はBookクラス。
    + `parameterType属性`は今回省略。メソッドの引数の型を書くらしい。省略すると、自動で実際の引数の型が判定される。
+ 下記`findById`では、WHERE句でEntityクラス（Bookクラス）のidフィールドを参照している。

```xml:BookDao.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.dao.BookDao">
    <select id="findById" resultType="com.example.demo.entity.Book">
        SELECT
        	id,
        	book_name,
        	volume_num,
        	author_name,
        	published_date
         FROM
         	booktable
         WHERE
         	id = #{id}
    </select>
    <select id="findAll" resultType="com.example.demo.entity.Book">
        SELECT
        	id,
        	book_name,
        	volume_num,
        	author_name,
        	published_date
         FROM
         	booktable
    </select>
</mapper>
```
## Serviceクラス
Daoクラスでも書いたが、1件検索メソッドではEntityクラスのidフィールドに値をセットしている。全件取得はEntityクラスを要素にもつリストを返す。

```java
package com.example.demo.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.dao.BookDao;
import com.example.demo.entity.Book;

@Service
public class BookService {

	@Autowired
	BookDao bookDao;

	//1件検索
	public Book findById(Integer id) {
		Book book = new Book();
		book.setId(id);
		return this.bookDao.findById(book);
	}

	//全件取得
	public List<Book> getBookList(){
		return this.bookDao.findAll();
	}
}
```
## Controllerクラス
下記では@GetMappingや@PostMappingを使いわけてません。

```java:BookController.java
package com.example.demo.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.entity.Book;
import com.example.demo.form.BookForm;
import com.example.demo.service.BookService;

@Controller
@RequestMapping("/book")
public class BookController {

	@Autowired
	BookService bookService;

	@RequestMapping("/search")
	public String index(BookForm bookForm, String showList, Model model) {

		//タイトル
		model.addAttribute("title", "本屋さん");

		//bookform（formクラス）がnullじゃなかったら1件検索
		if(bookForm.getId() != null) {
			Book book = bookService.findById(bookForm.getId());
			model.addAttribute("book", book);
		}

		//一覧表示ボタンが押されると本一覧をmodelに登録。
		if(showList != null) {
			List<Book> bookList = bookService.getBookList();
			model.addAttribute("bookList", bookList);
		}

		return "index";

	}

}

```

# 画面
`th:if`でControllerのModelに登録したオブジェクトがnullかどうかで表示する内容が変わります。

```html:index.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title th:text="${title}">title</title>
    <link href="/css/style.css" rel="stylesheet">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>
  	<p>本屋さん</p>
    <form action="/book/search" method="post">
		<label>ID:<input class="input" type="text" name="id"></label><br>
		<div><input class="search" type="submit" value="検索"/></div>
    </form>
    <form action="/book/search" method="post">
		<div><input class="list" type="submit" name="showList" value="一覧表示"/></div>
    </form>

    <div th:if="${book} !=null" th:object="${book}">
    	<table>
    		<tr>
    			<th>ID</th>
    			<th>書籍名</th>
    			<th>巻</th>
    			<th>著者名</th>
    			<th>刊行日</th>
    		</tr>
    		<tr>
    			<td th:text="*{id}">id</td>
				<td th:text="*{bookName}">書籍名</td>
				<td th:text="*{volumeNum}">巻</td>
				<td th:text="*{authorName}">著者名</td>
				<td th:text="*{publishedDate}">刊行日</td>
    		</tr>
    	</table>
    </div>

        <div th:if="${bookList} !=null">
    	<table>
    		<tr>
    			<th>ID</th>
    			<th>書籍名</th>
    			<th>巻</th>
    			<th>著者名</th>
    			<th>刊行日</th>
    		</tr>
    		<tr th:each="book:${bookList}" th:object="${book}">
    			<td th:text="*{id}">id</td>
				<td th:text="*{bookName}">書籍名</td>
				<td th:text="*{volumeNum}">巻</td>
				<td th:text="*{authorName}">著者名</td>
				<td th:text="*{publishedDate}">刊行日</td>
    		</tr>
    	</table>
    </div>
  </body>
</html>
```
CSSよく分かりません。タグを[type=" "]で区別できたり、table thとスペース区切りで子要素を指定できたりする、ということはわかりました。。。

```css:style.css
@charset "UTF-8";

input[type="text"]{
	width:70%;
	border-radius: 5px;
  	padding: 10px;
}
input[type="submit"].search{
	border-radius: 5px;
  	padding: 5px;
  	margin-top: 10px;
  	background-color:#99CCCC;
}

input[type="submit"].list{
	border-radius: 5px;
  	padding: 5px;
  	margin-top: 10px;
  	background-color: #008BBB;
  	color:#FFFFFF
}

table{
    width: 100%;
    margin-top: 10px;
    border-collapse: collapse;
}
table th, table td {
    border: 1px solid #ddd;
    padding: 6px;
}
table th {
  background-color: #6699FF;
}
```