Spring Bootでバリデーションを試してみた

# 背景・目的

前回、下記でデータの登録・更新・削除などを試しました。今回はバリデーションを試してみます
- [Spring Bootでデータを保存する-その2](https://qiita.com/zumax/items/06d0465218478840a949)
- [Spring Bootでデータを保存する-その1](https://qiita.com/zumax/items/2e387254c736659bf20c)

# まとめ
下記に特徴をまとめます

|特徴|説明|
|:--|:--|
|バリデーションのパッケージ|spring-boot-starter-validation|
|パッケージは二種類ある|・jakarta.validation<BR>・jakarta.validation.constraints|
|標準バリデーション|以降のものがある|
|入力されているかのバリデーション|```@Null```<BR>`@NotNull`<BR>`@NotBlank`<BR>`@NotEmpty`
|数値の範囲のバリデーション|`@Min`<BR>`@Max`<BR>`@DecimalMin`<BR>`@DecimalMax`|
|数値の桁数のバリデーション|`@Digits`|
|日時のバリデーション|`@Future`<BR>`@Past`|
|オブジェクト数|`@Size`|
|正規表現|`@Pattern`|
|バリデーションのカスタマイズ|エラーメッセージをカスタマイズするには、下記の方法がある<BR>・アノテーション内のmessageのバリューに記載する<BR>・resource/ValidationMessages.properties内に記載する<BR><BR>バリデーション自体を実装する|

# 概要
[Spring Boot 3 プログラミング入門](https://www.amazon.co.jp/Spring-Boot-3-%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E5%85%A5%E9%96%80-%E6%8E%8C%E7%94%B0%E6%B4%A5%E8%80%B6%E4%B9%83/dp/4798069167)を基に整理します

- SpringBootのバリデーションには、spring-boot-starter-validation というパッケージが用意されている

## validationのアノテーション
Springではバリデーションは、2種類用意されている。
- jakarta.validation
- jakarta.validation.constraints

|アノテーション |説明|備考|
|:--|:--|:--|
|`@Null`|Nullである|
|`@NotNull`|Nullではない|何も入力しない場合、空文字になるのですり抜けるので注意が必要|
|`@NotBlank`|空白ではない|入力フォームはこちらがよい|
|`@NotEmpty`|空ではない|NotBlankとの違いは、半角スペース等でも入力したとみなす|
|`@Min`|最小値||
|`@Max`|最大値||
|`@DecimalMin`|BigDecimal、int、long等値全般で使える最小値|
|`@DecimalMax`|BigDecimal、int、long等値全般で使える最大値|
|`@Digits`|整数と小数部分の桁数制限を行う|使い方。`@Digits(integer=5,fraction=10)`|
|`@Future`|現在より先の日時のみ受付可能|
|`@Past`|現在よりも過去の日時のみ受付可能|
|`@Size`|文字列、配列、コレクションなど、いくつもの値をまとめて保管するオブジェクトで使用する|`@Size(min=1,max=10)`|
|`@Pattern`|String項目で使う。正規表現のパターンを指定して入力チェックを行う|

### エラーメッセージ
- バリデーションで表示されるエラーメッセージは、あらかじめバリデーションパッケージに用意されている
- エラーメッセージをカスタマイズする場合は、messageを使用して表示すれば良い


### エラーメッセージをプロパティファイルで定義
- エンティティクラスのアノテーション部分にメッセージを直接記述するのは、メンテナンス性が良くない
- メッセージをプロパティにまとめて扱うのが良い
- resourcesフォルダに、ValidationMessages.propertiesに定義する

### オリジナルのバリデーター
- アノテーションで実装されたバリデーションは、バリデータと呼ばれるデータチェック用のクラスにより行われる
- 不足する場合は、自作も可能
- 自作には2つのクラスが必要
    - アノテーションクラス
    - バリデーションを行うバリデータクラス
- アノテーションクラス
    - `@interface アノテーション名とする`
    - java.lang.annotation.Annotationインタフェイスのサブクラスとして認識される
- バリデータクラス
    - jakarta.validation.ConstraintValidatorインタフェイスを実装する

# 実践
[Spring Boot 3 プログラミング入門](https://www.amazon.co.jp/Spring-Boot-3-%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E5%85%A5%E9%96%80-%E6%8E%8C%E7%94%B0%E6%B4%A5%E8%80%B6%E4%B9%83/dp/4798069167)を参考に試してみます。

## 前提
[Spring Bootでデータを保存する-その2(更新と削除)](https://qiita.com/zumax/items/06d0465218478840a949)で実装した内容を基にしています

## pom.xml
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation/3.3.1

1. 下記のdependencyを追加します
    ```
	<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
			<version>3.3.1</version>
	</dependency>
    ```
## バリデーションの追加
1. Personエンティティを開きます
1. 下記の追加を行います
    - NotBlank
    ```    
    @Column(length = 50,nullable = false)
    @NotBlank
    private String firstName;

    @Column(length = 50,nullable = false)
    @NotBlank
    private String lastName;
    ```

## ハンドラの修正
1. WebControllerのformメソッドを下記のように修正します
    ```
      @RequestMapping(value = "/", method=RequestMethod.POST)
      @Transactional
      public ModelAndView form(
        @ModelAttribute("formModel") @Validated Person person,
        BindingResult result,
        ModelAndView mav) {
        ModelAndView res = null;
    
        System.out.println(result.getFieldErrors());
    
        if(!result.hasErrors()) {
          repository.saveAndFlush(person);
          res = new ModelAndView("redirect:/");
        } else {
          mav.setViewName("index");
          mav.addObject("title","Test page");
          mav.addObject("message","Sorry, error is occured...");
          List<Person> list = repository.findAll();
          mav.addObject("people", list);
          res = mav;
        }
    
        return res;
      }
    ```
    - `@Validated`
        - バリデーションチェックのもの
        - エンティティの値をバリデーションチェックするためのもの
        - エンティティの各値を自動的にチェックする
    - バリデーションチェックのエラー
        - BindingResultで結果がわかる
        - hasErrorsメソッドでエラー有無がわかる
## テンプレートの修正
1. index.htmlを修正します
```
  <form method="post" action="/" th:object="${formModel}">
    <ul>
      <li th:each="error : ${#fields.detailedErrors()}" class="text text-danger" th:text="${error.message}" />
    </ul>

```

### 動作確認
1. ビルドとSpring Bootを起動します
    ```
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```
1. localhost:8080にアクセスします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/6bd1a160-bc18-67ff-4e1e-6485ea80fd32.png)

1. 空白のまま、「Create」をクリックします。エラーメッセージが表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/360ff681-b27d-e6f6-33a2-83b6f4acdc10.png)

1. test-taroとtest-suzukiを入力し、「Create」を入れると登録されました

## エラーメッセージの位置を変更する
各フィールド配下に、エラーメッセージを表示したほうがわかりやすいので改善します
1. index.htmlを下記のように修正します
    ```
      <form method="post" action="/" th:object="${formModel}">
        <style>.err {color:red}</style>
    
        <div class="mb-3">
          <label for="name" class="form-label">FirstName</label>
          <input type="text" class="form-control" name="firstName" th:value="*{firstName}" th:errorclass="err" />
          <div th:if="${#fields.hasErrors('firstName')}" th:errors="*{firstName}" th:errorclass="err"></div>
        </div>
        <div class="mb-3">
          <label for="name" class="form-label"></label>LastName</label>
          <input type="text" class="form-control" name="lastName" th:value="*{lastName}" th:errorclass="err" />
          <div th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}" th:errorclass="err"></div>
        </div>
        <div class="mb-3">
          <input type="submit" class="btn btn-primary" value="Create" />
        </div>
      </form>
    ```

### 動作確認
1. ビルドとSpring Bootを起動します
    ```
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```
1. localhost:8080にアクセスします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/6bd1a160-bc18-67ff-4e1e-6485ea80fd32.png)

1. 空白のまま、「Create」をクリックします。エラーになりました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/2a58f0e2-9301-5012-64e4-f69b5643fff0.png)

1. FirstNameだけ入力します。エラーになりました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/d4a21599-016a-95af-6792-5e4e4f30f274.png)


## エラーメッセージのカスタマイズ
バリデーションの引数を入れることで、メッセージをカスタマイズできます。

1. バリデーションのアノテーションにメッセージを指定します
    ```
        @Column(length = 50,nullable = false)
        @NotBlank(message="名前（FirstName）を入力してください！")
        private String firstName;
    
        @Column(length = 50,nullable = false)
        @NotBlank(message="名字（LastName）を入力してください！")
        private String lastName;
    ```

### 動作確認
1. ビルドしSpring Bootを再実行します
    ```
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```
1. localhost:8080にアクセスします
1. 何も入力せずに、「Create」をクリックすると、カスタマイズ後のエラーメッセージが表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/f9796ff2-9b87-6120-7b6a-991cb1e1630f.png)

## エラーメッセージのカスタマイズ（プロパティファイル）
エラーメッセージをハードコードするのではなく、プロパティファイルに書いておきます。

1. 先ほどのmessageを削除しておきます（エンティティ内のmessageが優先されるため）
    ```
    @Column(length = 50,nullable = false)
    @NotBlank
    private String firstName;

    @Column(length = 50,nullable = false)
    @NotBlank
    private String lastName;
    ```
1. ValidationMessages.propertiesをresourcesフォルダに配置します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/aed8ae90-a9ae-995d-dab8-ed9c0a634373.png)

1. 下記の設定を追記します
    ```
    jakarta.validation.constraints.NotBlank.message=must not be blank
    ```

### 動作確認
1. ビルドしSpring Bootを再実行します
    ```
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```

1. localhost:8080にアクセスします
1. 何も入力せずに、「Create」をクリックすると、プロパティファイルに設定されたエラーメッセージが表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/f40ee794-3df3-4cc8-bde2-8fba9a1c1643.png)

## バリデータの実装
### Validator
PhoneValidatorを実装する

1. ハンドラーと同様のパッケージにPhoneValidator.javaを追加します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/0a37ab27-f6fa-d82d-9ef6-1d6d9723f7db.png)

1. 下記のコードを追記します
    ```
    package com.example.data.rest_db;
    
    import jakarta.validation.ConstraintValidator;
    import jakarta.validation.ConstraintValidatorContext;
    
    public class PhoneValidator implements ConstraintValidator<Phone, String> {
    
        @Override
        public void initialize(Phone phone) {
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
          return value != null && value.matches("[0-9()-]*");
        }
    
    }
    ```
    - ConstraintValidatorインタフェイスの実装
        - パラメータは、Phoneと、String
            - Phoneがアノテーション
            - Stringが入力値
        - Stringを入力し、Phoneアノテーションによりバリデーションが設定される
        - initialize
            - 初期化メソッド
            - アノテーションの情報を取得できる
        - isValid
            - バリデーション
            - Stringで入力値、ConstraintValidatorContextが渡される

### アノテーション
1. ハンドラーと同様のパッケージに Phone.javaを追加します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/c19f156f-9e64-ae27-730f-34d3f4c9d7d6.png)

1. 下記のコードを追記します
    ```
    package com.example.data.rest_db;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    import jakarta.validation.Constraint;
    import jakarta.validation.Payload;
    
    @Documented
    @Constraint(validatedBy = PhoneValidator.class)
    @Target({ElementType.METHOD, ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Phone {
    
      String message() default "Invalid phone number";
    
      Class<?>[] groups() default {};
    
      Class<? extends Payload>[] payload() default {};
    
    }

    ```

### バリデータを使用する
#### テンプレートの修正
Phoneバリデータを使用します
1. index.htmlを修正します。Phoneを追加します
    ```
    <body class="container">
      <h1 class="display-4 mb-4" th:text="${title}"></h1>
      <p th:text="${message}"></p>
      <form method="post" action="/" th:object="${formModel}">
        <style>.err {color:red}</style>
    
        <div class="mb-3">
          <label for="name" class="form-label">FirstName</label>
          <input type="text" class="form-control" name="firstName" th:value="*{firstName}" th:errorclass="err" />
          <div th:if="${#fields.hasErrors('firstName')}" th:errors="*{firstName}" th:errorclass="err"></div>
        </div>
        <div class="mb-3">
          <label for="name" class="form-label"></label>LastName</label>
          <input type="text" class="form-control" name="lastName" th:value="*{lastName}" th:errorclass="err" />
          <div th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}" th:errorclass="err"></div>
        </div>
        <div class="mb-3">
          <label for="name" class="form-label"></label>Phone</label>
          <input type="text" class="form-control" name="phone" th:value="*{phone}" th:errorclass="err" />
          <div th:if="${#fields.hasErrors('phone')}" th:errors="*{phone}" th:errorclass="err"></div>
        </div>
        <div class="mb-3">
          <input type="submit" class="btn btn-primary" value="Create" />
        </div>
      </form>
    
      <table class="table">
        <thread>
          <tr>
            <th>firstName</th>
            <th>lastName</th>
            <th>Phone</th>
          </tr>
        </thread>
        <tbody>
          <tr th:each="person : ${people}">
            <td th:text="${person.firstName}"></td>
            <td th:text="${person.lastName}"></td>
            <td th:text="${person.phone}"></td>
          </tr>
        </tbody>
      </table>
    </body>
    ```

#### エンティティの修正
1. phone変数と、アクセスメソッド（SetterとGetter）を追加します。また、 作成した`@Phone`アノテーションを追加します
    ```
        @Column(nullable = false)
        @Phone
        private String phone; // Add this field
    
        public String getPhone() {
          return phone;
        }
        public void setPhone(String phone) {
          this.phone = phone;
        }
    ```

#### 初期値の設定
1. ハンドラのinitを修正します。phoneの初期値を埋めます
    ```
      public void init() {
        Person p1 = new Person();
        p1.setFirstName("Ichiro");
        p1.setLastName("Suzuki");
        p1.setPhone("00-0000-0000");
        repository.saveAndFlush(p1);
    
        Person p2 = new Person();
        p2.setFirstName("Taro");
        p2.setLastName("Yamada");
        p2.setPhone("00-0000-0001");
        repository.saveAndFlush(p2);
    
        Person p3 = new Person();
        p3.setFirstName("Hanako");
        p3.setLastName("Yamada");
        p3.setPhone("00-0000-0003");
        repository.saveAndFlush(p3);
    
      }
    ```

### 動作確認
1. ビルドしSpring Bootを再実行します
    ```
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```

1. localhost:8080にアクセスします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/522a4437-3079-cd63-ad3c-e9b7076e84fe.png)

1. Phoneに英字を入れます。エラーになりました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/bd13cf50-6133-b518-3ad2-1ac7c808b047.png)
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/27942aed-729d-e0c8-4e0c-57277428d77d.png)


# 考察
今回は、Spring Bootにおけるバリデーションについて、標準で用意されているものの利用から、エラーメッセージのカスタマイズ、バリデーションの自作まで試してみました。
引き続き、SpringBootの機能について学んでいきます。

# 参考
[Spring Boot 3 プログラミング入門](https://www.amazon.co.jp/Spring-Boot-3-%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E5%85%A5%E9%96%80-%E6%8E%8C%E7%94%B0%E6%B4%A5%E8%80%B6%E4%B9%83/dp/4798069167)


https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation/3.3.1