LoginSignup
8
4

More than 3 years have passed since last update.

【悲報】Spring Bootのバリデーションで壁にぶち当たる

Last updated at Posted at 2020-11-21

雑記です。
Spring-BootValidation機能を使って入力チェックするか~~と思っていたら壁にぶち当たりすぎました。特段ハイレベルなことをしているわけでもないのにどうしてこうなった。

それでは出来るようになるまでの道のりを辿っていきましょう。

やりたかったこと

  • フォームにバリデーションを設定
  • OKなら次のページへ遷移
  • NGなら元のURLに戻る
  • そしてエラーメッセージを表示

バリデーションの基本中の基本です。強いて言うなら元のURLに戻るってのが厄介かもね。

1つめの壁(オブジェクトが未定義)

フォーム。
名前と年齢の入力欄を作成。
エラーは入力欄の上に表示させたい。

loginform.html
<body>
<form th:action="@{/check}" method="post" th:object="${vf}">
<div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" style="color: red"></div>
名前(空白はNG)<input type="text" th:field="*{name}"><br>
<div th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color: red"></div>
年齢(18~25で入力してね)<input type="text" th:field="*{age}"><br>
<input type="submit" value="送信">
</form>
</body>

バリデーション用のクラス。
名前は空白NGhatoを含めること
年齢は18~25

ValidationForm.java
public class ValidationForm {
    @NotBlank
    @Pattern(regexp=".*hato+.*")
    private String name;
    @Min(18)
    @Max(25)
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

コントローラ。
http://localhost:8080にアクセス時、loginform.htmlを表示。
フォーム送信時、NG→元のページOK→menu.htmlへ飛ばす。

ValidationController.java
@Controller
public class ValidationController {
    @GetMapping("/")
    public String loginform() {
        return "loginform";
    }
    @PostMapping("/check")
    public String check(@Validated ValidationForm vf,BindingResult result) {
        if(result.hasErrors())return "redirect:/";
        return "menu";
    }

エラーメッセージ用プロパティファイル。

ValidationMessages.properties
javax.validation.constraints.Max.message={1}以下にして!
javax.validation.constraints.Min.message={1}以上にして!
javax.validation.constraints.NotBlank.message=空白はだめ!
javax.validation.constraints.Pattern.message={2}のパターンを含めて!

OK時の遷移先。
ただOKを表示するのみ。

menu.html
<body>
OK
</body>

一通りできたのでhttp://localhost:8080にアクセスすると、、、

見慣れた画面が出てきました(泣)
エラーを下まで遡ってみると、、、

Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "#fields.hasErrors('name')" (template: "loginform" - line 9, col 6)

なんとなくnameって名前のプロパティがない的なことを言ってそうです。
そりゃあ初回表示時はValidationFormクラスのオブジェクトは作られてないからなあ。どうしようもない。

a_validated_102.png
出典:漫画『三国志』

とりあえず色々調べてみると、オブジェクトを返すメソッドを定義して、そのメソッド自体に@ModelAttributeを付けてあげるといいらしい。

ValidationController.java
@ModelAttribute("vf")
public ValidationForm getValidationForm() {
    return new ValidationForm();
}

再度アクセス。

よかった、表示された。

教訓1
  • 初回表示時にオブジェクト未定義エラーを回避するにはオブジェクトを返すメソッドを定義して@ModelAttributeを付けること。

2つめの壁(リダイレクトの仕様)

とりあえず名前にhato、年齢に18(18歳じゃないけどね)を入力して送信を押すと、、、

OKOK、いい感じです。

じゃあエラーになる入力値で試してみましょう。まずは年齢を17に。

あれ?何も表示されない。

th:fieldを使っているから入力値が保持されているはずで
th:if="${#fields.hasErrors('age')}"がtrueになるからエラーメッセージも表示されるはず。

バリデーション自体は機能しているし、Whitelabel Error Pageも出ない。
なのになぜ???


出典:漫画『野原ひろし 昼メシの流儀』※一部セリフが変更されております。

調べた結果、原因はこれっぽい。

原因?
if(result.hasErrors())return "redirect:/";

ページ遷移に使われるforwardredirectですが、
前者はパラメータを受け渡せるのに対し、後者は渡せないと書いてありました。

種類 URL パラメータ
フォワード 変わらない 受け渡せる
リダイレクト 変わる 受け渡さない※

※後の章で出てきますが、ある機能で受け渡しが可能です。

forwardにして、ModelAndView追加して...とするのは面倒なので、loginformを指定するようにします。

書き換え
if(result.hasErrors())return "loginform";
教訓2
  • redirectforwardの性質についてきちんと理解すること。

3つめの壁(名前解決の罠)

やっとエラーが見れるようになるぞ...
気を取り直してhato14を入れてボタンを押す。

デジャヴ。またしても何も値が表示されない。
なんでっ・・・!どうしてっ・・・!


出典:漫画『賭博黙示録カイジ』

分からん時はとりあえずデバッグ。
よしよし、vfという名前でオブジェクトを定義していて、その中に入力値であるhato14が含まれているな。

ん?

お前デフォルト名のままやないかい!(Form名を先頭小文字にしたやつがデフォルトの名前)
@ModelAttribute@RequestParamみたいに自動で名前解決してくれるもんだと思ってた。

バリデーション時にModelAndViewaddObject()を使わなくてもパラメータの受け渡しができるのはBindingResultが内部でFormModelの情報を持っているからで、当然コイツがデフォルト名でFormを抱えていたらvfじゃ表示できないわけです。

@Validated@ModelAttributeは共存できるっぽいので、メソッドを書き換えます。

ValidationController.java
@PostMapping("/check")
// public String check(@Validated ValidationForm vf,BindingResult result) {
public String check(@Validated @ModelAttribute("vf") ValidationForm vf,BindingResult result) {
    if(result.hasErrors())return "loginform";
    return "menu";
}

再度実行してみましょう。

やっと・・・やっと表示されました・・・。


Form名もvfに書き換わっていますね。

教訓3
  • バリデーション用フォームをオリジナルの名前で定義するときは@ModelAttributeで名前を指定すること。

最後の壁(諦めたくない)

これでめでたしめでたし...ではありません。

やりたかったこと(再掲)
  •  フォームにバリデーションを設定
  •  OKなら次のページへ
  •  NGなら元のURLに戻る ← できてない!
  •  そしてエラーメッセージを表示

今のままではエラー時にURLがhttp://localhost:8080/checkとなってしまいます。これでも正しく動作するものの、「元のURLに戻す」という目標を諦めたくはありません。


出典:漫画『スラムダンク』

Redirectでパラメータを渡す方法さえあればどうにかできそうだと思って調べた結果、RedirectAttributesクラスを引数に追加することで渡せることを発見した。

ValidationController.java
//旧Code
//@PostMapping("/check")
//public String check(@Validated @ModelAttribute("vf") ValidationForm vf,BindingResult result) {
//  if(result.hasErrors())return "loginform";
//  return "menu";
//}
@PostMapping("/check")
public String check(@Validated @ModelAttribute("vf") ValidationForm vf,BindingResult result, RedirectAttributes attr) {
    if(result.hasErrors()) {
        attr.addFlashAttribute("vf",vf);
        attr.addFlashAttribute("vf",result);
        return "loginform";
    }
    return "menu";
}

さて、オブジェクトの受け渡しに使うModelAndView#addObjectの代役を果たしてくれるのがRedirectAttributes#addFlashAttributeなのだが、当然名前:値のペアで格納する。
Formの名前はvfでいいとして、BindingResultの方はどうするか...とりあえず一緒にvfを指定。

いつものアレが出てきたので没とします。
この後数時間ほど彷徨いましたが、心が折れる寸前で良さげな記事を見つけました。

:star:Spring MVCでPost-Redirect-Getパターンを実装する

記事の趣旨は元のURLに戻すではなくPost-Redirect-Getによる二重送信の防止のようですが、やりたいことは一致。

というわけでコードを見てみると、BindingResultがWebページに渡される際に、オブジェクト名の前にorg.springframework.validation.BindingResult.という接頭辞がつくそうです。道理でvfだけだとエラーになるわけだ...。

そしてこの接頭辞はBindingResult#MODEL_KEY_PREFIXで表現可能とのこと。さっそく書き換えましょ。

ValidationController.java
@PostMapping("/check")
public String check(@Validated @ModelAttribute("vf") ValidationForm vf,BindingResult result, RedirectAttributes attr) {
    if(result.hasErrors()) {
        attr.addFlashAttribute("vf",result);
        attr.addFlashAttribute(BindingResult.MODEL_KEY_PREFIX + "vf",result);
        return "redirect:/";
    }
    return "menu";
}

また、今回は使いませんでしたがConventions#getVariableName("オブジェクト名")で先頭小文字の名前(キャメルケース)に自動変換してくれるんですね。こちらも初耳。

それでは実行!

キターーーーーーーー(゚∀゚)ーーーーーーーー!!!!

紆余曲折ありましたが、ようやく最後の壁を突破しました。

教訓4
  • RedirectAttributesRedirectでもパラメータの受け渡しが可能
  • BindingResultを手動で渡す際の名前はBindingResult#MODEL_KEY_PREFIX + オブジェクト名を指定する

さいごに

次から次へと壁にぶち当たるこの状況は、かの勇将、山中鹿之助の生き様を見ているようですね。

願わくば、我に七難八苦を与えたまえ
――山中鹿之助

いやいや、こんな基本で躓きまくるのはもうこりごりです。

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4