Kotlin + SpringBoot + JPA + Thymeleafで簡単なCRUDを作る③～Validationの追加～

## 内容

- [前回](https://qiita.com/ozaki25/items/301d43dfcb1903ef995b)の続き
- [こちらの記事](https://qiita.com/ozaki25/items/49fb8c876c93611e228d)のKotlin版です
    - Kotlin初心者なのでもっと良い書き方あれば是非コメントで教えて下さい
- SpringBootで簡単なCRUDアプリを作る
- 今回は入力域にバリデーションをかける
    - 名前と年齢を必須項目にする
    - 年齢は0~150の範囲内にする
    - チーム名は20文字までにする

<img width="419" alt="スクリーンショット 2017-10-25 22.49.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/175213/33b9a050-6ba1-75f2-3106-7913522ff7f2.png">

## 手順

1. entityにバリデーションの設定を追加する
1. controllerにエラー制御を追加する
1. templateにエラーメッセージを追加する

### 1. entityにバリデーションの設定を追加する

- validationの設定はentityクラスの変数にアノテーションをつける
- `Player.kt`を編集する

```kotlin:src/main/kotlin/com/example/baseball/domain/Player.kt
package com.example.baseball.domain

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.validation.constraints.*

@Entity
data class Player(
        @Id
        @GeneratedValue
        val id: Long? = null,
        @get:NotEmpty // ①
        val name: String? = null,
        @get:NotNull // ②
        @get:Min(value = 0) // ③
        @get:Max(value = 150)
        val age: Int? = null,
        @get:Size(max = 20) // ④
        val team: String? = null,
        val position: String = null
)
```

- ①:`@get:NotEmpty`をつけるとnullと空文字の入力を許容しなくなる
- ②:`@get:NotNull`をつけるとnullの入力を許容しなくなる
- ③:最小値は`@get:Min`最大値は`@get:Max`で設定する
- ④:文字数の制限は`@get:Size`をつける

### 2. controllerにエラー制御を追加する

- エラーがあった場合にもとの画面に戻す処理を追加する
- `PlayerController.kt`を修正する

```kotlin:src/main/kotlin/com/example/baseball/controller/PlayerController.kt
    @GetMapping("new")
    fun newPlayer(model: Model): String {
        // ①
        model.addAttribute("player", Player())
        return "players/new"
    }

    @PostMapping
    // ②
    fun create(@Valid @ModelAttribute player: Player, bindingResult: BindingResult): String {
        // ③
        if (bindingResult.hasErrors()) return "players/new";
        playerService.save(player)
        return "redirect:/players"
    }

    @PutMapping("{id}")
    // ②
    fun update(@PathVariable id: Long, @Valid @ModelAttribute player: Player, bindingResult: BindingResult): String {
        // ③
        if (bindingResult.hasErrors()) return "players/edit";
        playerService.save(player.copy(id = id))
        return "redirect:/players"
    }
```

- ①:新規作成画面に対してPlayerインスタンスを渡すようにする
    - これがないと入力エラー時に入力していた内容を保持することができない
- ②:`player`に`@Valid`をつけることでvalidationチェック対象となる
    - アノテーションを横並びにしているが`@Valid`は`player`にかかっている

```kotlin
@Valid @ModelAttribute player: Player
// ↑と↓は同じ
@Valid
@ModelAttribute
player: Player
```
- ②:エラーがあると`bindingResult: BindingResult`の中にエラーの情報がセットされる
- ③:エラーがあると`bindingResult.hasErrors()`がtrueを返すのでその場合はもとの画面に返している

### 3. templateにエラーメッセージを追加する

#### new.html

- エラーがあってcontrollerに突き返された場合にエラーメッセージを表示するようにする

```html:src/main/resources/templates/players/new.html
      <!-- ① -->
      <form th:action="@{/players}" th:method="post" th:object="${player}">
        <!-- ② -->
        <div class="form-group" th:classappend="${#fields.hasErrors('name')}? has-error">
          <label class="control-label">名前</label>
          <input class="form-control" type="text" th:field="*{name}" />
          <!-- ③ -->
          <span class="text-danger" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span>
        </div>
        <div class="form-group" th:classappend="${#fields.hasErrors('age')}? has-error">
          <label class="control-label">年齢</label>
          <input class="form-control" type="number" th:field="*{age}" />
          <span class="text-danger" th:if="${#fields.hasErrors('age')}" th:errors="*{age}"></span>
        </div>
        <div class="form-group" th:classappend="${#fields.hasErrors('team')}? has-error">
          <label class="control-label">チーム名</label>
          <input class="form-control" type="text" th:field="*{team}" />
          <span class="text-danger" th:if="${#fields.hasErrors('team')}" th:errors="*{team}"></span>
        </div>
        <div class="form-group" th:classappend="${#fields.hasErrors('position')}? has-error">
          <label class="control-label">守備位置</label>
          <input class="form-control" type="text" th:field="*{position}" />
          <span class="text-danger" th:if="${#fields.hasErrors('position')}" th:errors="*{position}"></span>
        </div>
        <button class="btn btn-default" type="submit">作成</button>
      </form>
```

- ①:controllerからplayerを受け取るようになったので、th:objectにセット
    - 各inputフィールドもth:fieldを使うように修正([ここ](https://qiita.com/ozaki25/items/301d43dfcb1903ef995b#indexhtml)の③参照)
    - なぜこの変更を入れたかというとエラーがあって突き返された時に、入力内容をvalueにセットし直したいから
- ②:`th:classappend="${#fields.hasErrors('name')}? has-error"`の意味は、`#fields.hasErrors('name')`がtrueの時にclass属性にhas-errorを追加するよということ
    - `#fields`の中にエラーの情報が入っている
    - class属性にhas-errorをつけると、Bootstrapの定義によってラベルと枠が赤くなる
- ③:エラーメッセージを表示するための領域
    - th:ifが設定された要素は、th:ifの値がtrueの時(ここで言うとエラーがあった時)のみ表示される
    - `th:errors="*{name}"`は`*{name}`に対してセットされたエラーメッセージを全て表示させる
        - 今回の例では起きないが複数エラーがあったら改行して全て表示される

#### edit.html

- new.htmlと同様の処理を追加

```html:src/main/resources/templates/players/edit.html
      <form th:action="@{/players/{id}(id=*{id})}" th:method="put" th:object="${player}">
        <!-- ① -->
        <input type="hidden" th:field="*{id}">
        <div class="form-group" th:classappend="${#fields.hasErrors('name')}? has-error">
          <label class="control-label">名前</label>
          <input class="form-control" type="text" th:field="*{name}" />
          <span class="text-danger" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span>
        </div>
        <div class="form-group" th:classappend="${#fields.hasErrors('age')}? has-error">
          <label class="control-label">年齢</label>
          <input class="form-control" type="number" th:field="*{age}" />
          <span class="text-danger" th:if="${#fields.hasErrors('age')}" th:errors="*{age}"></span>
        </div>
        <div class="form-group" th:classappend="${#fields.hasErrors('team')}? has-error">
          <label class="control-label">チーム名</label>
          <input class="form-control" type="text" th:field="*{team}" />
          <span class="text-danger" th:if="${#fields.hasErrors('team')}" th:errors="*{team}"></span>
        </div>
        <div class="form-group" th:classappend="${#fields.hasErrors('position')}? has-error">
          <label class="control-label">守備位置</label>
          <input class="form-control" type="text" th:field="*{position}" />
          <span class="text-danger" th:if="${#fields.hasErrors('position')}" th:errors="*{position}"></span>
        </div>
        <button class="btn btn-default" type="submit">更新</button>
      </form>
```

- ①:Kotlin版ではidはhiddenで明示的に送らないと送信されない
    - これがないとバリデーションエラーで編集画面に戻されたときに送信先URLに入れるIDを特定できない

## 動作確認

- ここまで修正した状態で、http://localhost:8080/players にアクセスすると以下のような動作になっている

![demo2.gif](https://qiita-image-store.s3.amazonaws.com/0/175213/965c6be2-3071-f994-3f86-4d67ad3b96c2.gif)

## 次回

- 次は[こちら](https://qiita.com/ozaki25/items/203951e6bf6019b4425a)