検証、BindingResult、BeanValidation

> - ウェブサービスはフォーム入力時にエラーが発生すると、最初からフォームを再入力するようにしてはならず、顧客が入力したデータを維持した状態でどんなエラーが発生したのか親切に知らせなければならない。
> - これに関してスプリングでは、Binding ResultとBean Validationを提供し、便利に検証処理を行うようにする。

検証の種類は3つがある。
| 検証の種類 | 説明 |
| --- | --- |
| 1.タイプ検証|タイプによる検証 <br> ex.数字入力をすべきところに文字を入力していないか |
| 2. フィールド検証 | フィールドによる検証 <br> ex.商品名:必須、空白X <br> 価格:1000以上100万以下 <br> 数量: 最大 9999 |
| 3.特定フィールドの囲を超える検証|複合的な条件を考慮して検証 <br> ex.価格*数量の合計が10,000ウォン以上なのか|

# BindingResult
- 検証する@ModelAttribute のすぐ後ろに記す。
- Binding Resultを使用すると、1.タイプ検証でエラーが出てもエラーなしにコントローラが呼び出され、@ModelAttributeに対してBindingされた結果がBinding Resultに盛り込まれる。
- 以後2.フィールド検証及び3.特定フィールドの範囲を超える検証ができるようにする。
```java
@PostMapping("/add")
public String addItem(@ModelAttribute("item") ItemSaveForm form, 
											BindingResult bindingResult) {

    // 2. フィールド検証
    if (!StringUtils.hasText(item.getItemName())) {
    bindingResult.rejectValue("itemName", "required");
    }
    if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() >1000000) {
      bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
    }

    // 3.特定フィールドの囲を超える検証|複合的な条件を考慮して検証
    if (item.getPrice() != null && item.getQuantity() != null) {
    int resultPrice = item.getPrice() * item.getQuantity();
    if (resultPrice < 10000) {
        // rejectメソッドでエラーメッセージを作成
        bindingResult.reject("totalPriceMin"
                            , new Object[]{10000, resultPrice}, null);
        }
    }

    // 検証に失敗すると、再入力フォームへ
    if (bindingResult.hasErrors()) {
        return "/addForm";
    }
    ...
    return "redirect:/";
}
```
```html
<section id="error" th:if="${#fields.hasGlobalErrors()}">
    <p th:each="err : ${#fields.globalErrors()}" th:text="${err}"></p>
</section>

<!--#fields:Binding Resultが提供する検証エラーにアクセスできる。 -->
<!--globalErrors:"totalPriceMin"のようなグローバルエラーである。 -->
<!--参考までに、"itemName"、"price"は当該フィールドに対するエラーである。-->
```

# BeanValidation
- Annotationを通じて便利に2.フィールド検証をより簡単にできるようにする。
```gradle
    build.gradleに追加    
    implementation 'org.springframework.boot:spring-boot-starter-validation'
```
- @NotNull、@NotBlank など検証したいものに応じてAnnotation を付ける。
```java
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

public class Item {

    @NotNull
    private Long id;
    // 空値 + 空白のみの場合を許可しません。
    @NotBlank
    private String itemName;
    // nullを許可しません。
    @NotNull
    // minとmaxの範囲内の値でなければなりません。
    // Rangeはhibernateでのみ動作します。 (import参考)
    @Range(min = 1000, max = 1000000)
    private Integer price;
    // メッセージを使ってメッセージを決めることもできます。
    @NotNull(message = "nullX")
    // 最大9999まで許可します。
    @Max(value = 9999)
    private Integer quantity;
    ...
}
```
- @ModelAttribute または @Request Body の前に @Validated を付ける。
```java
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item
    , BindingResult bindingResult
    , RedirectAttributes redirectAttributes) {

    ...

    // 既存に長く書いた以下のコードを作成しなくてもよい。
    // 2.フィールド検証
    // if (!StringUtils.hasText(item.getItemName())) {
    // bindingResult.rejectValue("itemName", "required");
    // }
    // if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() >1000000) {
    // bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}、 null);
    // }

    //検証に失敗すると、再度入力フォームへ
    if (bindingResult.hasErrors()) {
        return "/addForm";
    }
    ...
    return "redirect:/";
}
```
```html
<form action="item.html" th:action th:object="${item}" method="post">
    <input type="text" th:field="*{itemName}" th:errorclass="field-error">
    <div class="field-error" th:errors="*{itemName}">

    <input type="text" th:field="*{price}" th:errorclass="field-error">
  <div class="field-error" th:errors="*{price}">
</form>
```

- th:errorclass 
    - th:fieldで指定したフィールドにエラーがあれば、class情報を追加する。
- th:errors
    - 該当フィールドにエラーがある場合にタグを出力する。 th:ifの便宜バージョン

# reject(), rejectValue()
- 上記の例を見ると、メッセージを作成するためにreject()とrejectValue()を使用していた。
1. reject()
    - 3.特定フィールドの範囲を超える検証に使用する
    ```java
    bindingResult.reject(
        "totalPriceMin", new Object[]{10000, resultPrice}, null);
    ```
    ```properties
    errors.properties

    totalPrice Min=全体の価格は{0}以上である必要があります。 現在値 = {1}
    
    参考までに、errors.propertiesを使用する場合、application.propertiesにerrorを追加しなければならない。
    spring。messages.basename=messages,errors
    ```
    
2. rejectValue()
    - 2.フィールド検証に使用する。
    ```java
    bindingResult.rejectValue(
		"price", "range", new Object[]{1000, 10000000}, null);
    ```
    ```properties
    errors.properties

    NotBlank.item.itemName=商品名を書いてください。
    NotBlank={0}空白X             {0}はフィールド名
    
    Range={0}、{2}~{1}の許容
    Max={0}、最大{1}
    ```

# エラーコード

- 上記の例を見ると、以下のようにエラーコードを作成していた。
- NotBlankのように汎用性を持って作成することもでき、NotBlank.item.itemNameのように細かく作成することもできる。 細かく作成したNotBlank.item.itemNameが優先順位を持つようになり、なければNotBlankを使うことになる。
    ```properties
    errors.properties

    NotBlank.item.itemName=商品名を書いてください。
    NotBlank={0}空白X             {0}はフィールド名
    ```
- オブジェクトエラーの場合、code.objectName の順に作成する。
    - ex. required.item
- フィールドエラーの場合、code.object Name.field / code.field / code.field Type / code の順に作成する。
    - ex. NotBlank.item.itemName / NotBlank。itemName / NotBlank。java.lang.String / NotBlank