雑記です。
Spring-Boot
のValidation
機能を使って入力チェックするか~~と思っていたら壁にぶち当たりすぎました。特段ハイレベルなことをしているわけでもないのにどうしてこうなった。
それでは出来るようになるまでの道のりを辿っていきましょう。
#やりたかったこと
- フォームにバリデーションを設定
- OKなら次のページへ遷移
- NGなら元のURLに戻る
- そしてエラーメッセージを表示
バリデーションの基本中の基本です。強いて言うなら元のURLに戻る
ってのが厄介かもね。
#1つめの壁(オブジェクトが未定義)
フォーム。
名前と年齢の入力欄を作成。
エラーは入力欄の上に表示させたい。
<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>
バリデーション用のクラス。
名前は空白NG
、hatoを含めること
年齢は18~25
。
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
へ飛ばす。
@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";
}
エラーメッセージ用プロパティファイル。
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を表示するのみ。
<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
クラスのオブジェクトは作られてないからなあ。どうしようもない。
とりあえず色々調べてみると、オブジェクトを返すメソッドを定義して、そのメソッド自体に@ModelAttribute
を付けてあげるといいらしい。
@ModelAttribute("vf")
public ValidationForm getValidationForm() {
return new ValidationForm();
}
- 初回表示時にオブジェクト未定義エラーを回避するにはオブジェクトを返すメソッドを定義して
@ModelAttribute
を付けること。
#2つめの壁(リダイレクトの仕様)
とりあえず名前にhato
、年齢に18
(18歳じゃないけどね)を入力して送信を押すと、、、
OKOK、いい感じです。
じゃあエラーになる入力値で試してみましょう。まずは年齢を17
に。
あれ?何も表示されない。
th:field
を使っているから入力値が保持されているはずで
th:if="${#fields.hasErrors('age')}"
がtrueになるからエラーメッセージも表示されるはず。
バリデーション自体は機能しているし、Whitelabel Error Pageも出ない。
なのになぜ???
出典:漫画『野原ひろし 昼メシの流儀』※一部セリフが変更されております。
調べた結果、原因はこれっぽい。
if(result.hasErrors())return "redirect:/";
ページ遷移に使われるforward
とredirect
ですが、
前者はパラメータを受け渡せるのに対し、後者は渡せないと書いてありました。
種類 | URL | パラメータ |
---|---|---|
フォワード | 変わらない | 受け渡せる |
リダイレクト | 変わる | 受け渡さない※ |
※後の章で出てきますが、ある機能で受け渡しが可能です。 |
forward
にして、ModelAndView
追加して...とするのは面倒なので、loginform
を指定するようにします。
if(result.hasErrors())return "loginform";
######教訓2
-
redirect
、forward
の性質についてきちんと理解すること。
#3つめの壁(名前解決の罠)
やっとエラーが見れるようになるぞ...
気を取り直してhato
、14
を入れてボタンを押す。
デジャヴ。またしても何も値が表示されない。
なんでっ・・・!どうしてっ・・・!
出典:漫画『賭博黙示録カイジ』
分からん時はとりあえずデバッグ。
よしよし、vf
という名前でオブジェクトを定義していて、その中に入力値であるhato
と14
が含まれているな。
ん?
お前デフォルト名のままやないかい!(Form名を先頭小文字にしたやつがデフォルトの名前)
@ModelAttribute
や@RequestParam
みたいに自動で名前解決してくれるもんだと思ってた。
バリデーション時にModelAndView
やaddObject()
を使わなくてもパラメータの受け渡しができるのはBindingResult
が内部でForm
とModel
の情報を持っているからで、当然コイツがデフォルト名でForm
を抱えていたらvf
じゃ表示できないわけです。
@Validated
と@ModelAttribute
は共存できるっぽいので、メソッドを書き換えます。
@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";
}
再度実行してみましょう。
やっと・・・やっと表示されました・・・。
######教訓3
- バリデーション用フォームをオリジナルの名前で定義するときは
@ModelAttribute
で名前を指定すること。
#最後の壁(諦めたくない)
これでめでたしめでたし...ではありません。
######やりたかったこと(再掲)
- フォームにバリデーションを設定
- OKなら次のページへ
- NGなら元のURLに戻る ← できてない!
- そしてエラーメッセージを表示
今のままではエラー時にURLがhttp://localhost:8080/check
となってしまいます。これでも正しく動作するものの、「元のURLに戻す」という目標を諦めたくはありません。
出典:漫画『スラムダンク』
Redirectでパラメータを渡す
方法さえあればどうにかできそうだと思って調べた結果、RedirectAttributes
クラスを引数に追加することで渡せることを発見した。
//旧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
を指定。
いつものアレが出てきたので没とします。
この後数時間ほど彷徨いましたが、心が折れる寸前で良さげな記事を見つけました。
Spring MVCでPost-Redirect-Getパターンを実装する
記事の趣旨は元のURLに戻す
ではなくPost-Redirect-Getによる二重送信の防止
のようですが、やりたいことは一致。
というわけでコードを見てみると、BindingResult
がWebページに渡される際に、オブジェクト名の前にorg.springframework.validation.BindingResult.
という接頭辞がつくそうです。道理でvf
だけだとエラーになるわけだ...。
そしてこの接頭辞はBindingResult#MODEL_KEY_PREFIX
で表現可能とのこと。さっそく書き換えましょ。
@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
-
RedirectAttributes
でRedirect
でもパラメータの受け渡しが可能 -
BindingResult
を手動で渡す際の名前はBindingResult#MODEL_KEY_PREFIX + オブジェクト名
を指定する
#さいごに
次から次へと壁にぶち当たるこの状況は、かの勇将、山中鹿之助の生き様を見ているようですね。
願わくば、我に七難八苦を与えたまえ
――山中鹿之助
いやいや、こんな基本で躓きまくるのはもうこりごりです。