NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

相関チェックで学ぶ初心者向けバリデーション

JavaのSpringフレームワークを使ったバリデーション実装方法をアウトプットしています。@AssertTrueとValidatorクラスを用いた相関チェックの実装を紹介!

はじめに

入社2年目の冬爪です。初めての後輩ができ、先輩になったな~とやっと実感がわいてきました!!
今回はJavaのSpringフレームワークを使ったバリデーションの実装を社内研修で学んだのでアウトプットします。 本記事では料金の範囲検索を実装する際の相関チェックを、AssertTrueアノテーションとValidatorクラスの2パターンで実装してみます。

今回のゴール

今回のゴールは最小入力値と最大入力値に相関チェックを設け、以下の画像のようにエラーメッセージを表示させることです。(画像はフィールドエラーの場合)

相関チェック_フィールドエラー

パターン①:@AssertTrueを用いる方法

一つ目はFormクラスに@AssertTrueの付与したメソッドを実装して、相関チェックを自作する方法です。

1. BlogPriceFormクラスの実装

相関チェックを行いたい画面で使用するFormクラスで @AssertTrueを付与したboolean型のメソッドを作成します。 @AssertTrueの引数としてmessagesを設定することで任意のメッセージを表示させることが可能です。

import jakarta.validation.constraints.AssertTrue;
import lombok.Data;

@Data
public class BreadSearchForm {
  //値段最小入力値
    private Integer priceMin;
  //値段最大入力値
    private Integer priceMax;

 @AssertTrue(message = "入力された数値は不適切です")
 public boolean isPriceValidated() {
     if(priceMin==null || priceMax==null) {
         return true;
     }
     if(priceMin <= priceMax) {
         return true;
     }else {
         return false;
     }
  }
}

上記のように@AssertTrueを付与したbooleanメソッドを実装することで、自由にバリデーションチェックを作成することが可能になります。

2. Controllerクラスの実装
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import lombok.RequiredArgsConstructor;

@Controller
@RequiredArgsConstructor
public class BlogController {
    
    @GetMapping("/blog/list")
    public String initial(BlogPriceForm form,Model model){
        return "blog/blogFieldList";
    }

    @PostMapping("/blog/list/search")
    public String search(@ModelAttribute @Validated BlogPriceForm form, BindingResult bindingResult) {
            return "blog/fieldErrorBread";
    }

}

BlogPriceForm@AssertTrueを使用してバリデーションチェックを行うので、 searchメソッドの方では使用するBlogPriceForm@Validated付けることを忘れないようにしましょう。

3. Thymeleafの実装
<!DOCTYPE html>
<html>

<head>
   <meta charset="UTF-8">
   <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.9.3/dist/semantic.min.css">
   <title>パンの商品一覧</title>
   <style>
        .textColor{color: red;}
    </style>
</head>

<body>
    <div class="ui container">
        <form th:action="@{/blog/list/search}" method="post" th:object="${blogPriceForm}" id="sendForm">
            <div>
                <span th:text="#{price}"></span>
                <input type="text" th:field="*{priceMin}">
                <span th:text="#{tilde}"></span>
                <input th:errorclass="error" 
                    th:class="${#fields.hasErrors('priceValidated')}? fieldError" 
                    type="text" th:field="*{priceMax}">
                <span class="textColor" th:errors="*{priceValidated}"></span>
            </div>
            <button>
                <span>送信</span>
                <i class="paper plane icon" style="visibility: visible;"></i>
            </button>
        </form>
    </div>
</body>

</html>

BlogPriceFormではisPriceValidatedという名前で相関チェックを実装したので、このメソッドをバリデーションチェックで使用した場合はisを省いて先頭文字を小文字にした、priceValidatedとします。
上記の場合、フィールドエラー扱いとなり、入力ボックス付近にエラーが表示されます。
 

パターン② Validatorクラスを用いる方法

二つ目の方法はValidatorクラスを作成し、Controller内のメソッドを呼び出す前にController内で相関チェック処理を行う方法です。 今回は以下の公式ドキュメントを参考にグローバルエラーとフィールドエラーの2パターンを実装してみます。

spring.pleiades.io

1. Validatorクラスの実装
1_1. グローバルエラー
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

@Component
public class BreadSearchValidator implements Validator{
  
 @Override
    public boolean supports(Class<?> clazz) {
        return clazz.equals(BreadSearchForm.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        final var form = (BreadSearchForm) target;
         if(form.getPriceMin()!=null && form.getPriceMax()!=null) {
            if(form.getPriceMin() > form.getPriceMax()) {
              //rejectメソッドを使用するとグローバルエラー
              errors.reject("priceError", "入力された数値は不適切です"); 
            }
        }
     }
}
1_2. フィールドエラー
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

@Component
public class BreadSearchValidator implements Validator{
  
 @Override
    public boolean supports(Class<?> clazz) {
        return clazz.equals(BreadSearchForm.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        final var form = (BreadSearchForm) target;
         if(form.getPriceMin()!=null && form.getPriceMax()!=null) {
            if(form.getPriceMin() > form.getPriceMax()) {
              //rejectValueメソッドを使用するとフィールドエラー。
              errors.rejectValue("priceMax","errorCode","入力された数値は不適切です"); 
            }
        }
     }
}

supportsメソッドで、バリデーションのチェックをしたいクラスかどうかを判定します。
今回はBreadSearchFormを対象にチェックするよう記載しています。
その後、validateメソッドで検証を行います。 引数のtargetはObject型でリクエストされた情報がすべて入っているので、値を取得するためにform型にキャストしています。
グローバルエラーの場合はrejectメソッドを、フィールドエラーの場合はrejectValueメソッドを使用します。
フィールドエラーの場合はエラーコードを指定して表示させることもできますが、今回はエラーコードを特に指定せず、文字列を渡してメッセージを表示させています。

2. Controllerクラスの実装
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Controller
@RequiredArgsConstructor
public class BlogController {
    
    private final BlogGlobalErrorValidator blogGlobalErrorValidator;
    
    @InitBinder
    public void initBinder(WebDataBinder binder){
        //グローバルエラーの場合
        binder.addValidators(blogGlobalErrorValidator);
        //フィールドエラーの場合
        binder.addValidators(blogFieldErrorValidator);
    }
    
    //グローバルエラーの場合
    @GetMapping("/blog/globalList")
    public String initial1(BlogPriceForm form){
        return "blog/blogGlobalList";
    }
    
    @PostMapping("/blog/global/search")
    public String getListGlobalError(@ModelAttribute @Validated BlogPriceForm form, BindingResult bindingResult) {
            return "blog/blogGlobalList";
    }
    
    //フィールドエラーの場合
    @GetMapping("/blog/fieldList")
    public String initial2(BlogPriceForm form){
        return "blog/blogFieldList";
    }
    
    @PostMapping("/blog/field/search")
    public String getListFieldError(@ModelAttribute @Validated BlogPriceForm form, BindingResult bindingResult) {
            return "blog/blogFieldList";
    }

}

上記では、グローバルエラーの場合とフィールドエラーの場合の両方を記載しています。 initBinderメソッドは引数の数の分だけ呼ばれます。(上記の場合はValidatorクラスのtargetと、BreadSearchFormの2回)

3. Thymeleafの実装
3_1. グローバルエラー
<!DOCTYPE html>
<html>

<head>
   <meta charset="UTF-8">
   <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.9.3/dist/semantic.min.css">
   <title>パンの商品一覧</title>
   <style>
        .textColor{color: red;}
    </style>
</head>

<body>
    <div class="ui container">
        <form th:action="@{/blog/global/search}" method="post" th:object="${blogPriceForm}" id="sendForm">
            <div th:if="${#fields.hasGlobalErrors()}">
                <p class="textColor" th:each="err : ${#fields.globalErrors()}" th:text="${err}"></p>
            </div>
            <div>
                <span th:text="#{price}"></span>
                <input type="text" th:field="*{priceMin}">
                <span th:text="#{tilde}"></span>
                <input type="text" th:field="*{priceMax}">
            </div>
            <button>
                <span>送信</span>
                <i class="paper plane icon" style="visibility: visible;"></i>
            </button>
        </form>
    </div>
</body>

</html>

グローバルエラーの場合、テキストボックスの横ではなく、フォームの上部にエラーメッセージが表示されます。

3_2. フィールドエラー
<!DOCTYPE html>
<html>

<head>
   <meta charset="UTF-8">
   <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.9.3/dist/semantic.min.css">
   <title>パンの商品一覧</title>
   <style>
        .textColor{color: red;}
    </style>
</head>

<body>
    <div class="ui container">
        <form th:action="@{/blog/field/search}" method="post" th:object="${blogPriceForm}" id="sendForm">
            <div>
                <span th:text="#{price}"></span>
                <input type="text" th:field="*{priceMin}">
                <span th:text="#{tilde}"></span>
                <input th:errorclass="error" type="text" th:field="*{priceMax}">
                <span class="textColor" th:errors="*{priceMax}"></span>
            </div>
            <button>
                <span>送信</span>
                <i class="paper plane icon" style="visibility: visible;"></i>
            </button>
        </form>
    </div>
</body>

</html>

パターン①と同様に、フィールドエラーの場合、テキストボックスの横にエラーメッセージが表示されます。

まとめ

今回は社内の研修で学んだことをアウトプットも兼ねてまとめてみました! フィールドエラーとグローバルエラーはエラーの種類が異なっているので、エラー時に希望する挙動に合わせて使い分けてみるといいと思います。 個人的にはValidatorを使用した方が、他画面の同じような相関チェックにも流用できる、かつ、フィールドエラーにするかグローバルエラーにするか状況によって決定できるので便利だと思いました。 どなたかの参考になれば幸いです。

執筆者:冬爪
新人エンジニア ポケモンだいすき✨