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


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

それでは出来るようになるまでの道のりを辿っていきましょう。
#やりたかったこと
- フォームにバリデーションを設定
- OKなら次のページへ遷移
- NGなら元のURLに戻る
- そしてエラーメッセージを表示

バリデーションの基本中の基本です。強いて言うなら`元のURLに戻る`ってのが厄介かもね。
#１つめの壁（オブジェクトが未定義）
フォーム。
名前と年齢の入力欄を作成。
エラーは入力欄の上に表示させたい。

```html: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>
```
バリデーション用のクラス。
名前は`空白NG`、`hatoを含めること`
年齢は`18～25`。

```java: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`へ飛ばす。

```java: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";
	}
```
エラーメッセージ用プロパティファイル。

```properties: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を表示するのみ。

```html:menu.html
<body>
OK
</body>
```
一通りできたので`http://localhost:8080`にアクセスすると、、、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/c7244eba-4be3-c020-fd0f-2504077414d4.png" width="75%">
見慣れた画面が出てきました（泣）
エラーを下まで遡ってみると、、、

>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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/db4e6dce-5f9f-1a1a-17cf-6dc44ec36c6d.png)
出典：漫画『三国志』

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

```java:ValidationController.java
@ModelAttribute("vf")
public ValidationForm getValidationForm() {
	return new ValidationForm();
}
```
再度アクセス。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/dc2dfa37-fa7b-c0d0-03b0-a25688c7aef5.png" width="75%">
よかった、表示された。
######教訓１
- 初回表示時にオブジェクト未定義エラーを回避するにはオブジェクトを返すメソッドを定義して`@ModelAttribute`を付けること。

#２つめの壁（リダイレクトの仕様）
とりあえず名前に`hato`、年齢に`18`（18歳じゃないけどね）を入力して送信を押すと、、、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/67cfa21d-d81e-77a7-026f-b8c9eea42327.png" width="75%">
OKOK、いい感じです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/c5a3aeae-eda3-0bbb-784f-cdbfa7de6154.png" width="75%">
じゃあエラーになる入力値で試してみましょう。まずは年齢を`17`に。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/da2f40b9-8ca8-bd64-ee07-0f0bd6101c42.png" width="75%">
あれ？何も表示されない。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/84817526-5365-15ce-dea7-c27c2ddafe38.png" width="75%">
`th:field`を使っているから入力値が保持されているはずで
`th:if="${#fields.hasErrors('age')}"`がtrueになるからエラーメッセージも表示されるはず。

バリデーション自体は機能しているし、Whitelabel Error Pageも出ない。
なのになぜ？？？
><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/07c4e84d-0c4d-4eaa-22d7-89d2f6404baa.png" width="80%">
出典：漫画『野原ひろし 昼メシの流儀』※一部セリフが変更されております。

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

```java:原因？
if(result.hasErrors())return "redirect:/";
```
ページ遷移に使われる`forward`と`redirect`ですが、
前者はパラメータを受け渡せるのに対し、後者は渡せないと書いてありました。

|種類|URL|パラメータ|
|:-:|:-:|:-:|
|フォワード|変わらない|受け渡せる|
|リダイレクト|変わる|受け渡さない※|
※後の章で出てきますが、ある機能で受け渡しが可能です。

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

```java:書き換え
if(result.hasErrors())return "loginform";
```

######教訓2
- `redirect`、`forward`の性質についてきちんと理解すること。

#３つめの壁（名前解決の罠）
やっとエラーが見れるようになるぞ...
気を取り直して`hato`、`14`を入れてボタンを押す。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/39082a9c-48eb-8812-5e3f-f516312cd493.png" width="75%">
デジャヴ。またしても何も値が表示されない。
なんでっ・・・！どうしてっ・・・！
><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/74d6d851-62e0-16c9-59af-4c2e22dadda9.png" width="80%">
出典：漫画『賭博黙示録カイジ』

分からん時はとりあえずデバッグ。
よしよし、`vf`という名前でオブジェクトを定義していて、その中に入力値である`hato`と`14`が含まれているな。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/77c80ef6-6d03-bfb1-67aa-676ddd539406.png" width="75%">
ん？
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/83567f40-d288-14e4-ef30-6f5261e49c11.png" width="75%">
お前デフォルト名のままやないかい！（Form名を先頭小文字にしたやつがデフォルトの名前）
`@ModelAttribute`や`@RequestParam`みたいに自動で名前解決してくれるもんだと思ってた。

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

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

```java: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";
}
```

再度実行してみましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/3227ade5-68ef-1590-2285-c256c5b05030.png" width="75%">
やっと・・・やっと表示されました・・・。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/e11ee1f9-f96f-4d5f-b143-fc28ef7ab478.png" width="75%">
`Form`名も`vf`に書き換わっていますね。

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

#最後の壁（諦めたくない）
これでめでたしめでたし...ではありません。
######やりたかったこと（再掲）

- [x] 　フォームにバリデーションを設定
- [x] 　OKなら次のページへ
- [ ] 　NGなら元のURLに戻る　<font color="red">←　できてない！</font>
- [x] 　そしてエラーメッセージを表示

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

><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/2e24d328-1feb-c0ff-6fc6-5b726cb62375.png" width="75%">
出典：漫画『スラムダンク』

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

```java: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`を指定。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/03547d5f-3b03-1abf-66f8-4d8112ebe475.png" width="70%">
いつものアレが出てきたので没とします。
この後数時間ほど彷徨いましたが、心が折れる寸前で良さげな記事を見つけました。

:star:[Spring MVCでPost-Redirect-Getパターンを実装する](https://qiita.com/unhurried/items/de32dfe5c87643491e54)

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

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

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

```java: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("オブジェクト名")`で先頭小文字の名前（キャメルケース）に自動変換してくれるんですね。こちらも初耳。

それでは実行！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624514/56731d97-5542-3467-ec0d-dd0bbf330f3d.png" width="70%">
ｷﾀｰｰｰｰｰｰｰｰ(ﾟ∀ﾟ)ｰｰｰｰｰｰｰｰ!!!!

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

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

#さいごに
次から次へと壁にぶち当たるこの状況は、かの勇将、山中鹿之助の生き様を見ているようですね。
>願わくば、我に七難八苦を与えたまえ
――山中鹿之助

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