はじめに
入社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パターンを実装してみます。
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を使用した方が、他画面の同じような相関チェックにも流用できる、かつ、フィールドエラーにするかグローバルエラーにするか状況によって決定できるので便利だと思いました。 どなたかの参考になれば幸いです。