Spring Boot 2 + Thymeleaf + MyBatis で CRUD アプリ作成

# 開発環境

- Java11
- Spring Boot2
- H2
- MyBatis3
- Thymeleaf3
- JQuery
- Bootstrap
- Intellij IDEA

## 開発環境の構築

- 上記であらかじめインストールの必要があるのは以下となる。
    - Java11
    - IDE(ここではIntellijでの説明を行う。他のIDEでも可。)

# プロジェクトの作成

- Intellijから、`ファイル` → `新規` → `プロジェクト`で、`Spring Initilizr`を選択する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/a06eb7ee-e89b-cb03-d510-e18ddd9173d2.png)

- Java11を選択する。（たぶんJava8でも大丈夫）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/202a4019-27eb-457f-1a41-40e840972014.png)

- DevTools、Web、Thymeleaf、H2、MyBatisをチェックする。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/7bcdfe78-fbc4-a6cf-0ffc-7c3049a9dbc8.png)

- プロジェクト名を入力して、完了。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/0fc3bbf1-3dc5-3c50-11e8-a9e44f0d19d8.png)

### ホットデプロイの設定1/2

- 開発効率を上げるため、ソースを変更したときに自動的にビルドする、ホットデプロイの設定を行う。
    - 大きな変更がある場合や変更が反映されない場合は、サーバーの停止→開始をしたほうがいい。
- Intellijの設定から、`ビルド、実行、デプロイ` → `コンパイラー` から、`□自動的にプロジェクトをビルドする`にチェックを入れる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/f4d3a5a6-a3c2-18fd-c52e-b1ffe538aa28.png)

### ホットデプロイの設定2/2

- `ctrl+shift+a` → `レジストリ`と入力して、`レジストリ...`をクリック。（Intellijを日本語化していない場合は、`registry`と入力して、`Registry...`をクリック）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/8e9950ac-7064-9f10-0aac-4ca9b424baa1.png)

- compiler. ... .running ☑にチェックを入れる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/4a13ec4f-b17a-6cf7-ceb4-9363a9740582.png)

# pom.xmlの修正

- tymeleafのレイアウト機能を使いたい場合は、以下のようにpom.xmlの`<dependencies></dependencies>`内に`thymeleaf-layout-dialect`を入れる。（ここでは入れることを前提に進める。）
- デザインの見栄えを良くするために、BootstrapとJQueryを入れる。
- pom.xmlを右クリック→再インポートを行う。

```xml:pom.xml
    <dependency>
      <groupId>nz.net.ultraq.thymeleaf</groupId>
      <artifactId>thymeleaf-layout-dialect</artifactId>
      <version>2.3.0</version>
    </dependency>
    <dependency>
      <groupId>org.webjars</groupId>
      <artifactId>jquery</artifactId>
      <version>1.11.1</version>
    </dependency>
    <dependency>
      <groupId>org.webjars</groupId>
      <artifactId>bootstrap</artifactId>
      <version>3.3.7-1</version>
    </dependency>
```

# schema.sql, data.sql作成

- schema.sql, data.sqlを作成するとプロジェクト実行開始時に自動的にDBにテストデータを投入してくれる。
- プロジェクト名/src/main/resourcesの下に作成する。

```sql:schema.sql
CREATE TABLE IF NOT EXISTS user (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name varchar (50) NOT NULL
);
```

```sql:data.sql
INSERT INTO user (name) VALUES ('tanaka');
INSERT INTO user (name) VALUES ('suzuki');
INSERT INTO user (name) VALUES ('sato');
```

# entity作成

- entityパッケージを作成して、その配下にUserクラスを作成する。
- Userデータを保持するクラスを作成。MyBatisを使うためのアノテーションはいらない。
- nameに必須入力のバリデーションを追加する。

```java:com.example.demo.entity.User.java
package com.example.demo.entity;


import org.hibernate.validator.constraints.NotBlank;

public class User {

  private int id;

  @NotBlank(message = "{require_check}")
  private String name;

  public int getId() {
    return id;
  }

  public void setId(int id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

}
```
# mapper.java , mapper.xml作成

- mapperパッケージを作成して、その配下にUserMapperインターフェースを作成する。
- アノテーションは、`@Mapper`をつける。このままだと、Autowiredができないので、`@Repository`もつける。

```java:src/main/java/com.example.demo.mapper.UserMapper.java
package com.example.demo.mapper;

import com.example.demo.entity.User;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

@Repository
@Mapper
public interface UserMapper {

  List<User> selectAll();

  User select(int id);

  int insert(User user);

  int update(User user);

  int delete(int id);
}
```

- XMLでのマッピングファイルは、resouces配下にJavaと同じディレクトリ階層で作成すると自動的に読みに行ってくれる。
    - namespace: 上記(java)のパッケージ名
    - id: 上記(java)で指定したメソッド名
    - resultType: 返却する型

```xml:src/main/resouces/com/example/demo/mapper/UserMapper
<?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.mapper.UserMapper">
    <select id="selectAll" resultType="com.example.demo.entity.User">
        select * from user
    </select>
    <select id="select" resultType="com.example.demo.entity.User">
        select * from user where id = #{id}
    </select>
    <insert id="insert">
        insert into user (name) values (#{name})
    </insert>
    <update id="update">
        update user set name = #{name} where id = #{id}
    </update>
    <delete id="delete">
        delete from user where id = #{id}
    </delete>
</mapper>
```

# controller作成

- 7つのアクションを追加する。

|HTTPメソッド|URL|controllerのメソッド|説明|
|----|----|----|----|
|GET|/users|getUsers()|一覧画面の表示|
|GET|/users/1|getUser()|詳細画面の表示|
|GET|/users/new|getUserNew()|新規作成画面の表示|
|POST|/users|postUserCreate()|新規作成画面の挿入処理|
|GET|/users/1/edit|getUserEdit()|編集画面の表示|
|PUT|/users/1|putUserEdit()|編集画面の更新処理|
|DELETE|/users/1|deleteUser()|削除処理|


```java:com.example.demo.controller.UserController
package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import java.util.List;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/users")
public class UserController {

  private final UserMapper userMapper;

  @Autowired
  public UserController(UserMapper userMapper) {
    this.userMapper = userMapper;
  }

  /**
   * 一覧画面の表示
   */
  @GetMapping
  public String getUsers(Model model) {
    List<User> users = userMapper.selectAll();
    model.addAttribute("users", users);
    return "users/list";
  }

  /**
   * 詳細画面の表示
   */
  @GetMapping("{id}")
  public String getUser(@PathVariable int id, Model model) {
    User user = userMapper.select(id);
    model.addAttribute("user", user);
    return "users/show";
  }

  /**
   * 新規作成画面の表示
   */
  @GetMapping("new")
  public String getUserNew(Model model) {
    User user = new User();
    model.addAttribute("user", user);
    return "users/new";
  }

  /**
   * 新規作成画面の挿入処理
   */
  @PostMapping
  public String postUserCreate(@ModelAttribute @Valid User user,
      BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
      return "users/new";
    }
    userMapper.insert(user);
    return "redirect:/users";
  }

  /**
   * 編集画面の表示
   */
  @GetMapping("{id}/edit")
  public String getUserEdit(@PathVariable int id, Model model) {
    User user = userMapper.select(id);
    model.addAttribute("user", user);
    return "users/edit";
  }

  /**
   * 編集画面の更新処理
   */
  @PutMapping("{id}")
  public String putUserEdit(@PathVariable int id, @ModelAttribute @Valid User user,
      BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
      return "users/edit";
    }
    user.setId(id);
    userMapper.update(user);
    return "redirect:/users";
  }

  /**
   * 削除処理
   */
  @DeleteMapping("{id}")
  public String deleteUser(@PathVariable int id, Model model) {
    userMapper.delete(id);
    return "redirect:/users";
  }
}
```

# バリデーションメッセージの設定（messages.properties、WebConfig.java作成）

- バリデーションメッセージを設定するために、messages.propertiesを作成する。

```properties:src/main/resources/messages.properties
name=ユーザー名
require_check={0}は必須入力です
```

- 上記プロパティファイルを使用するために、以下の設定Javaファイルを作成するというおまじない。

```java:com.example.demo/WebConfig.java
package com.example.demo;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class WebConfig {

  @Bean
  public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource bean = new ReloadableResourceBundleMessageSource();
    bean.setBasename("classpath:messages");
    bean.setDefaultEncoding("UTF-8");
    return bean;
  }

  @Bean
  public LocalValidatorFactoryBean localValidatorFactoryBean() {
    LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
    localValidatorFactoryBean.setValidationMessageSource(messageSource());
    return localValidatorFactoryBean;
  }
}
```

# 画面の作成(layout,show,list,new,edit,form,css作成)

### 共通レイアウト(layout.html)

- `<div layout:fragment="contents"></div>`内にコンテンツが表示される。

```html:src/main/resources/templates/commons/layout.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" >

<head>
    <meta charset="UTF-8">
    <link th:href="@{/webjars/bootstrap/3.3.7-1/css/bootstrap.min.css}" rel="stylesheet"/>
    <script th:src="@{/webjars/jquery/1.11.1/jquery.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/3.3.7-1/js/bootstrap.min.js}"></script>
    <link th:href="@{/css/home.css}" rel="stylesheet"/>
    <title>Home</title>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container-fluid">
        <div class="navbar-header">
            <a class="navbar-brand" href="#">SpringMyBatisDemo</a>
        </div>
    </div>
</nav>

<div class="container-fluid">
    <div class="row">
        <div class="col-sm-2 sidebar">
            <ul class="nav nav-pills nav-stacked">
                <li role="presentation"><a th:href="@{/users}">ユーザー管理</a></li>
            </ul>
        </div>
    </div>
</div>

<div class="container-fluid">
    <div class="row">
        <div class="col-sm-10 col-sm-offset-2 main">
            <div layout:fragment="contents">

            </div>
        </div>
    </div>
</div>

</body>
</html>
```

### 一覧画面(list.html)

-  `layout:decorate="~{commons/layout}"`でレイアウトが使用される。
-  `<div layout:fragment="contents"></div>`で囲まれた部分が、レイアウト内に表示される。

```html:src/main/resources/templates/users/list.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{commons/layout}">

<head>
  <meta charset="UTF-8">
  <link th:href="@{/webjars/bootstrap/3.3.7-1/css/bootstrap.min.css}" rel="stylesheet"/>
  <script th:src="@{/webjars/jquery/1.11.1/jquery.min.js}"></script>
  <script th:src="@{/webjars/bootstrap/3.3.7-1/js/bootstrap.min.js}"></script>
  <link th:href="@{/css/home.css}" rel="stylesheet"/>
  <title>Home</title>
</head>
<body>
<div layout:fragment="contents">
  <div class="page-header">
    <h1>ユーザー一覧</h1>
  </div>
  <table class="table table-bordered table-hover table-striped">
    <tr>
      <th class="info col-sm-2">ユーザーID</th>
      <th class="info col-sm-2">ユーザー名</th>
      <th class="info col-sm-2"></th>
    </tr>
    <tr th:each="user:${users}">
      <td th:text="${user.id}"></td>
      <td th:text="${user.name}"></td>
      <td>
        <a class="btn btn-primary" th:href="@{'/users/' + ${user.id}}">詳細</a>
        <a class="btn btn-primary" th:href="@{'/users/' + ${user.id} + '/edit'}">編集</a>
      </td>
    </tr>
  </table>
  <label class="text-info" th:text="${result}">結果表示</label><br/>
  <a class="btn btn-primary" th:href="${'/users/new'}">新規作成</a>
</div>

</body>
</html>
```

### 詳細画面(show.html)

-  `layout:decorate="~{commons/layout}"`でレイアウトが使用される。
-  `<div layout:fragment="contents"></div>`で囲まれた部分が、レイアウト内に表示される。
-  `<div th:include="users/form :: form_contents"></div>`に共通フォームが表示される。

```html:src/main/resources/templates/users/show.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{commons/layout}">

<head>
  <meta charset="UTF-8"/>
  <title></title>
</head>
<body>
<div layout:fragment="contents">
  <div class="row">
    <div class="col-sm-5">
      <div class="page-header">
        <div>
          <h1>ユーザー詳細</h1>
        </div>
          <table class="table table-bordered table-hover">
            <tr>
              <th class="active col-sm-3">ユーザー名</th>
              <td>
                <div class="form-group" th:object="${user}">
                  <label class="media-body" th:text="*{name}">
                  </label>
                </div>
              </td>
            </tr>
          </table>
        <form th:action="@{/users}" th:method="get">
          <button class="btn btn-primary btn-lg pull-right" type="submit">一覧</button>
        </form>
      </div>
    </div>
  </div>
</div>
</body>
</html>
```

### 編集画面(edit.html)

-  `layout:decorate="~{commons/layout}"`でレイアウトが使用される。
-  `<div layout:fragment="contents"></div>`で囲まれた部分が、レイアウト内に表示される。
-  `<div th:include="users/form :: form_contents"></div>`に共通フォームが表示される。

```html:src/main/resources/templates/users/edit.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{commons/layout}">

<head>
  <meta charset="UTF-8"/>
  <title></title>
</head>
<body>
<div layout:fragment="contents">
  <div class="row">
    <div class="col-sm-5">
      <div class="page-header">
        <div>
          <h1>ユーザー詳細</h1>
        </div>
        <form th:action="@{/users/{id}(id=*{id})}" th:method="put" th:object="${user}">

          <div th:include="users/form :: form_contents"></div>

          <button class="btn btn-primary btn-lg pull-right" type="submit">更新</button>
        </form>
        <form th:action="@{/users/{id}(id=*{id})}" th:method="delete" th:object="${user}">
          <button class="btn btn-danger btn-lg" type="submit">削除</button>
        </form>
      </div>
    </div>
  </div>
</div>
</body>
</html>
```

### 新規画面(new.html)

-  `layout:decorate="~{commons/layout}"`でレイアウトが使用される。
-  `<div layout:fragment="contents"></div>`で囲まれた部分が、レイアウト内に表示される。
-  `<div th:include="users/form :: form_contents"></div>`に共通フォームが表示される。

```html:src/main/resources/templates/users/new.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{commons/layout}">

<head>
  <meta charset="UTF-8"/>
  <title></title>
</head>
<body>
<div layout:fragment="contents">
  <div class="row">
    <div class="col-sm-5">
      <div class="page-header">
        <div>
          <h1>ユーザー新規作成</h1>
        </div>
        <form method="post" th:action="@{/users}" th:object="${user}">

          <div th:include="users/form :: form_contents"></div>

          <button class="btn btn-primary btn-lg pull-right" type="submit" name="update">作成</button>
        </form>
      </div>
    </div>
  </div>
</div>
</body>
</html>
```

### 共通フォーム

-  `<div th:fragment="form_contents"></div>`内が共通フォームで、`<div th:include="users/form :: form_contents"></div>`で埋め込むことができる。

```html:src/main/resources/templates/users/form.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
  <meta charset="UTF-8"/>
  <title></title>
</head>
<body>
<div th:fragment="form_contents">
  <table class="table table-bordered table-hover">
    <tr>
      <th class="active col-sm-3">ユーザー名</th>
      <td>
        <div class="form-group" th:classappend="${#fields.hasErrors('name')} ? 'has-error'">
          <label>
            <input type="text" class="form-control" th:field="*{name}"/>
          </label>
          <span class="text-danger" th:if="${#fields.hasErrors('name')}"
                th:errors="*{name}">Name error</span>
        </div>
      </td>
    </tr>
  </table>
</div>
</body>
</html>
```

### css

```css:src/main/resources/static/css/home.css
body {
    padding-top: 50px;
}

.sidebar {
    position: fixed;
    display: block;
    top: 50px;
    bottom: 0;
    background-color: #F4F5F7;
}

.main {
    padding-top: 50px;
    padding-left: 20px;
    position: fixed;
    display: block;
    top: 0;
    bottom: 0;
}

.page-header {
    margin-top: 0;
}
```

- http://localhost:8080/users を開くと、以下のような一覧画面が表示される。
- 一覧画面

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/15a7b4de-4b50-4b74-518a-2ea62a4c1a3c.png)

- 編集画面

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/86ad0488-002f-2b40-dc75-d460ecca8079.png)

- 未入力エラー表示

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/9bad75df-78f4-b04b-caac-b32f1246eba1.png)

- ソースコード
https://github.com/tseno/SpringMyBatisDemo

- 参考
    - [Spring解体新書](https://www.amazon.co.jp/%E3%80%90%E5%BE%8C%E6%82%94%E3%81%97%E3%81%AA%E3%81%84%E3%81%9F%E3%82%81%E3%81%AE%E5%85%A5%E9%96%80%E6%9B%B8%E3%80%91Spring%E8%A7%A3%E4%BD%93%E6%96%B0%E6%9B%B8-Boot2%E3%81%A7%E5%AE%9F%E9%9A%9B%E3%81%AB%E4%BD%9C%E3%81%A3%E3%81%A6%E5%AD%A6%E3%81%B9%E3%82%8B%EF%BC%81Spring-Security%E3%80%81Spring-JDBC%E3%80%81Spring-MyBatis%E3%81%AA%E3%81%A9%E5%A4%9A%E6%95%B0%E8%A7%A3%E8%AA%AC%EF%BC%81-ebook/dp/B07H6XLXD7)
    - [SpringBoot + JPA + Thymeleafで簡単なCRUDを作る②～画面と機能作成まで～](https://qiita.com/ozaki25/items/3b348874b6db5ab4f04f)