バリデーション

バリデーションとは

「Spring Bootの基本的な処理の流れ」の単元では、基本のパラメータの渡し方について学習しました。
この単元では、バリデーションについて学んでいきます。

Webアプリにおけるバリデーションとは、入力されたデータや送信されたデータが適切かどうかをチェックすることを言います。
例えば、下の画像はログイン画面の例で、未入力のチェックや入力形式のチェックを行い、ユーザーに入力し直すように伝えています。

入力チェックには、クライアントサイド(Javascript)でのチェックと、サーバーサイドのチェックがあります。クライアントサイドでチェックを行えば、サーバと通信する前にチェックしてエラーメッセージを出すことなどもできユーザビリティの向上も図れますが、クライアントサイドでは改ざんのリスクがあります。そのため、最後の関門であるサーバーサイドのチェックは必須です。

基本のバリデーション

Springが提供するバリデーション機能Bean ValidationないしHibernate Validatorを活用してバリデーションを実装します。Hibernate Validatorは、Bean Validationの仕様を実際に実装したライブラリで、さらに独自の拡張機能も提供しています。ルーツは大きく変わりませんので基本の使い方は同様で、特に違いを意識することも少ないでしょう。
いずれもプロジェクト作成時に導入したライブラリspring-boot-starter-validationに含まれています。pom.xmlの下記の記述です。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

フォーム送信されたパラメータをバリデーションし、ブラウザにバリデーションメッセージを表示するまでの主なポイントは以下3点です。

  • Formクラスのメンバにアノテーションを付与してバリデーションを適用する
  • Controllerメソッドに@Validated、BindingResultを記載
  • Viewファイルにバリデーションメッセージを表示する記述を追加

ここでは「Spring Bootの基本的な処理の流れ」の実践②(@Formクラス Ver.)で作成したファイルに対してバリデーションを追加していきます。
完成形は下記の画像のように、バリデーションチェックに引っかかった場合はエラーメッセージと共に再度index2の画面が表示されます。

Formクラスへの追記

ManualFormに追記します。
【com/cmps/spring/form/ManualForm.java】

package com.cmps.spring.form;

import jakarta.validation.constraints.NotEmpty;////追記
import jakarta.validation.constraints.NotNull;////追記
import org.hibernate.validator.constraints.Length;////追記
import org.hibernate.validator.constraints.Range;////追記
import lombok.Data;

@Data
public class ManualForm implements Serializable {

	// 名前
	@NotEmpty(message = "名前は必須項目です。")////追記
	@Length(max = 10, message = "名前は10文字以内で入力してください。") //// 追記
	private String userName;

	// 出身
	@NotEmpty(message = "出身は必須項目です。")////追記
	private String comeFrom;

	// 年齢
	@NotNull(message = "年齢は必須項目です。")////追記
	@Range(min = 0, max = 130, message = "年齢は0~130で入力してください。")////追記
	private Integer age;
}

アノテーションを付与したメンバに対して、バリデーションルールを適用できます。
@NotEmpty:必須チェック(NULL、空文字の場合NG)
@Length:文字数チェック(文字列の文字数範囲を設定できる)
@NotNull:必須チェック(NULLの場合NG。String型に使用すると空文字列を許容する)
@Range:整数の範囲チェック
ちなみにjakarta.validationパッケージはBean Validation、org.hibernate.validatorパッケージはHibernate Validatorです。
引数にmesssageを指定するとエラーメッセージを個別に設定できます。設定しない場合はデフォルトの英語のエラーメッセージが出力されます。Spring Bootではエラーメッセージを一括で日本語化するライブラリなどは用意されていませんが、設定ファイルにまとめて記述することはできます。 ☆後ほど紹介します。

Controllerへの追記

ManualControllerに追記します。
【com/cmps/spring/controller/ManualController.java】

package com.cmps.spring.controller;

import org.springframework.stereotype.Controller;// ・・以下略

import org.springframework.validation.BindingResult;////追記
import org.springframework.validation.annotation.Validated;////追記

@Controller
public class ManualController {

	/**
	 * (フォーム①RequestParam)初期画面の表示~入力内容の確認  省略
	 */

	/**
	 * 初期画面の表示(フォーム②Formクラス)
	 * 
	 * @param model Model
	 * @return "manual/index2" String viewファイル
	 */
	@GetMapping("/manual/index2")
	public String index2(Model model
		@ModelAttribute("form") ManualForm form) {////追記

		// 表示するViewを指定
		return "manual/index2";
	}

	/**
	 * 入力内容の確認(フォーム②Formクラス)
	 * 
	 * @param model Model
	 * @param form ManualForm form入力データ
	 * @return "manual/check2" String viewファイル
	 */
	@PostMapping("/manual/check2")
	public String check2(Model model,
			@ModelAttribute("form") @Validated ManualForm form,////追記ここから
			BindingResult result) {
		if (result.hasErrors()) {
			return index2(model, form);
		}////追記ここまで

		// 表示するViewを指定
		return "manual/check2";
	}
}

バリデーションを実行し、エラーがあった場合に異なる処理を行わせるには、form送信先にあたるメソッド(ここではcheck2)に下記の記述が必須です。
@Validatedアノテーションの追加
②@Validatedの直後にBindingResultを引数として設定
エラーがあった場合の処理を記述

①@Validatedアノテーションの追加 と ②@Validatedを付与したオブジェクトの直後にBindingResultを引数として配置 は、
public String check2(Model model,
@ModelAttribute("form") @Validated ManualForm form,////追記ここから
BindingResult result) {

の部分です。①の@Validatedがあることがバリデーションを実行する目印になります。②のBindingResultとは、Errorsというインターフェースを継承するサブインターフェースで、バリデーションの結果情報を持ちます。

③エラーがあった場合の処理 として、if (result.hasErrors()) { というif文が書かれています。BindingResultのhasErrors()メソッドは名前の通りバリデーションエラーがあればtrue、なければfalseを返します。
このif文の中の処理 return index2(model, form);return "manual/index2"; と書いても基本的に問題ありません。ただし、index2メソッドでmodelに渡している変数がある場合は、後者の場合はcheck2メソッド内でも渡す必要が出てきます。前者のように呼び出した方が繰り返しの記述をする手間が省ける可能性があります。

続いて、index2メソッドにも @ModelAttribute("form") ManualForm form を追記しております。
これは、次のViewの追記にも関連しますが、バリデーションのエラーメッセージを表示するにあたり、check2メソッドからView”manual/index2″に、ModelAttributeとしてformを渡して使用することに起因します。”manual/index2″はオブジェクトformが渡されることが前提のViewとなるので、index2メソッドで画面表示する場合にも「formが存在しない」というエラーが発生しないように追記しています。

Viewへの追記

index2.htmlに追記します。
【src/main/resources/templates/manual/index2.html】

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ja">
<head>
	<meta charset="UTF-8">
	<title>Spring Bootの基本的な処理の流れ</title>
	<style>
		.formParts {
			width: 300px;
		}
		li {
			list-style-type: none;
			padding-top: 5px;
		}
		.error_msg {/* 追記①ここから */
			color: red;
		}
		.error_form {
			border: aps solid red;
		}/* 追記①ここまで */
	</style>
</head>
<body>
	<h1>Spring Bootの基本的な処理の流れ</h1>
	<form method="post" th:action="@{/manual/check2}" accept-charset="UTF-8" th:object="${form}"><!-- 追記②th:object,th:field -->
		<h2>フォーム②(Formクラス)</h2>
		<p th:if="${#fields.hasErrors()}" class="error_msg">入力内容に誤りがあります。</p><!-- 追記③ -->
		<ul>
			<li>
				名前:
				<input class="formParts" th:errorclass="error_form" type="text" name="userName" th:field="*{userName}" value="名前"><!-- 追記⑤th:errorclass -->
				<span th:if="${#fields.hasErrors('userName')}" th:errors="*{userName}" class="error_msg"></span><!-- 追記④ -->
			</li>
			<li>出身:
				<select class="formParts" th:errorclass="error_form" name="comeFrom"><!-- 追記⑤th:errorclass -->
					<option value="">選択してください</option>
					<option value="Japan" th:selected="*{comeFrom == 'Japan'}">日本</option><!-- 追記⑥th:field -->
					<option value="other" th:selected="*{comeFrom == 'other'}">日本以外</option><!-- 追記⑥th:field -->
				</select>
				<span th:if="${#fields.hasErrors('comeFrom')}" th:errors="*{comeFrom}" th:errorclass="error_msg"></span><!-- 追記④ -->
			</li>
			<li>年齢:
				<input class="formParts" th:errorclass="error_form" type="text" name="age" th:field="*{age}" value="20"><!-- 追記⑤th:errorclass -->
				<span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" th:errorclass="error_msg"></span><!-- 追記④ -->
			</li>
			<li>
				<input type="submit" value="送信ボタン">
			</li>
		</ul>
	</form>
</body>
</html>

追記①~⑤について説明します。
追記①:CSSの内容で、error時の見た目を変えるために追加しています。

追記②:th:object="${form}" の追加
これはエラーメッセージ出力のために必要な記述です。この子クラス内でエラーに関する記載が生きます。

追記③: <p th:if="${#fields.hasErrors()}" class="error_msg">入力内容に誤りがあります。</p>
#fieldsはフォームのフィールドに関連するエラーをチェックしたり、表示したりするためのヘルパーオブジェクトです。基本的に、バリデーション返却されたオブジェクトを指定したth:object配下で使用できます。
#fields.hasErrors()は、このように引数が空の場合は「バリデーション全体でエラーがあるかないか」を真偽値で返却するので、ここでは「何かしらのバリデーションエラーがある場合にこの要素を表示する」という挙動です。

追記④:<span th:if="${#fields.hasErrors('userName')}" th:errors="*{userName}" class="error_msg"></span>
③と類似の#fields.hasErrors(‘userName‘)という記述がありますが、これはuserNameフィールドにエラーがある場合にtrueです。
th:errors=”*{userName}” は、userNameにエラーがあった場合、この要素のテキストにエラーメッセージを挿入します。

追記⑤:
th:errorclass="error_form" の追加
th:errorclassは、フィールドにバリデーションエラーがある場合にのみ、指定したクラスを適用します。(ここではborderを赤にするクラス)
th:field="*{~}" の追加(テキストボックス:name、age)
th:fieldを指定すると、バリデーションエラーにかかった場合に、前のページの入力値を保持して表示します。
例えば年齢のテキストボックスに「ああ」などの文字列を入力してエラーになった場合も、古い入力値をテキストボックスに自動で表示してくれます。
(補足:thymeleaf・Spring Bootの環境でth:fieldを設定すると、name属性の代わりも果たしてくれるので、name属性は省略可能になります。ただし、他の言語やFWでは同様とは限らないので、「フォーム部品にはname属性が必須」という意識をつけるためにも、省略せずに書くようにすることをおすすめします。)

追記⑥:th:selected="*{comeFrom == 'Japan'}" の追加(セレクトボックス)
追記⑤の応用です。セレクトボックスでは、selectタグに th:field=”*{~}” を指定しても反映されません。
セレクトボックスで、バリデーション時にユーザー選択状態で表示するには、今回のように指定します。
*{comeFrom == 'Japan'} の部分は条件式になっていますので、trueまたはfalseが返ります。
つまり selected=”true” または selected=”false” と出力されます。前者であればselectedが有効になるので、バリデーションエラー時に選択状態となるのです。

以上の記述でバリデーションが適用されました。ブラウザで確認してみてください。
うまく反映されない場合はSpring Bootアプリケーションを再起動してください。

バリデーションの追加知識

バリデーションのアノテーション一覧(抜粋)

比較的良く使うアノテーションを紹介します。

@Null
@NotNull
それぞれ「値がnullである」「値がnullでない」ということをチェックする。
ただし、String値の場合は「空文字列」と判定されて機能しないので、@NotEmptyや@NotBlankを使うと良い。Integer型には未入力チェックとして有効
@Min(value)
@Max(value)
数値(整数)に対し、最小値または最大値を指定する。
@Size(min=○,max=○) 配列、コレクションなどで使われ、そのオブジェクトに保管される要素数の下限または上限を指定する。文字列型に使用すると文字数チェック。
@PositiveOrZero値が正数か0であることをチェックする。
@Digits(integer, fraction)  値が指定した整数部の桁数および小数部の桁数以内であることをチェックする。
@Future
@Past
日付値が未来の日付であることをチェックする。
日付値が過去の日付であることをチェックする。
@EmailString値に入力された値が電子メールアドレスかどうかをチェックする。
@Pattern(regexp=○)String値の項目に対し、正規表現のパターンに応じてチェックする。

公式:パッケージ jakarta.validation.constraints
公式:Package org.hibernate.validator.constraints
参考:Spring Frameworkで使用できるBean Validationアノテーション一覧

Viewでバリデーションメッセージをまとめて表示するサンプル

前章では個別にバリデーションメッセージを表示する例を示しました。ここではまとめて表示する例を紹介します。
以下のコードをth:objectにFormクラスを指定したformタグの子要素として配置しましょう。(タグ配下であればどこでもOK)
本文で扱った個別のメッセージは残したままで問題ありません。

<ul th:if="${#fields.hasErrors()}" class="error_form">
	<li th:each="err : ${#fields.allErrors()}" th:text="${err}" class="error_msg"></li>
</ul>

fields.allErrors()メソッドを利用してすべてのエラーを取得し、th:each属性を用いて繰り返し処理を行い liタグを生成しています。

エラーメッセージを設定ファイルにまとめて記述する場合

①「src/main/resources」下の「application.properties」に追記(読み込むファイルの指定とエンコーディングの指定)

# validation messageファイルの設定
spring.messages.basename=ValidationMessages
spring.messages.encoding=UTF-8

②「src/main/resources」フォルダに「ValidationMessages.properties」ファイルを作成し、所定の記述を行う

# ========================
# バリデーションエラーメッセージ
# ========================

#### パターン1 各バリデーション属性(パッケージ名から指定)のmessaegeを固定
jakarta.validation.constraints.NotEmpty.message={0}は必須項目です
jakarta.validation.constraints.NotNull.message={0}は必須項目です
org.hibernate.validator.constraints.EMail={0}はメールアドレスの形式で入力してください
org.hibernate.validator.constraints.Range.message={0}は{2}~{1}の間で入力してください
## エラー対応(typeMismatch+フィールドのデータ型)
# 型の不一致の場合(文字列がIntegerに変換できない)
typeMismatch.java.lang.Integer=数値で入力してください。
typeMismatch.keel.validation.value.MailAddress=メールアドレス形式(sample@example.com)で入力してください。

## メモ
# {0} : プロパティ名 
# アノテーションの属性値は、{1}以降に埋め込まれる。インデックス位置はアノテーションの属性名のアルファベット順(昇順)。
# 上記のRangeの例の場合
# {1} : max属性の値
# {2} : min属性の値

## フォーム部品の表示名を指定(指定しないとname属性のまま出力される)
userName=ユーザー名
comeFrom=出身
age=年齢

設定ファイルに記述することでFormクラスに個別に記入する必要がなくなりますので、基本的にこちらの方が好ましいです。
※Formクラスのアノテーションにmesssage属性でバリデーションメッセージを指定している場合、Formクラスの方が優先されます。

③プロジェクトの更新を行うと、適用されるようになります。(「Springの概要とプロジェクトの作成」参照)

参考:#13 Spring エラーメッセージの表示 #Java – Qiita

相関バリデーション

画面から入力された複数のデータにまたがって、データの正当性を検証することを相関チェックや相関バリデーションと呼びます。
例えば「パスワード」と「パスワード確認用の再入力」が一致するか、といった場合に使用します。

相関バリデーションの実装には@AssertTrueアノテーションを使います。
@AssertTrueアノテーションは、対象のフィールドがtrueでなければならないことを示す役割を持ちます。

実際に試してみましょう。
今回は開始日が終了日よりも後になった場合、エラーメッセージが出るようにします。

これまで使ってきたファイルに追記していきます。
・index2.htmlファイル(Formタグ内)

	<!-- 追記 ここから -->
	<li>
		期間:
		<input class="formParts" type="text" name="lower" th:errorclass="error_form" th:field="*{lower}">~
		<input class="formParts" type="text" name="upper" th:errorclass="error_form" th:field="*{upper}">
		<span th:if="${#fields.hasErrors('lower')}" th:errors="*{lower}" th:errorclass="error_msg"></span>
		<span th:if="${#fields.hasErrors('upper')}" th:errors="*{upper}" th:errorclass="error_msg"></span>
		<span th:if="${#fields.hasErrors('dateValid')}" th:errors="*{dateValid}" th:errorclass="error_msg"></span>
	</li>
	<!-- 追記 ここまで -->

・ManualForm.javaファイル

    // 追記 ここから
    // 期間(開始日)
    @NotNull(message = "開始日は入力必須です。")
    private Integer lower;

    // 期間(終了日)
    @NotNull(message = "終了日は入力必須です。")
    private Integer upper;

    @AssertTrue(message = "開始日は終了日以前を入力してください。")
    public boolean isDateValid() {
        if (lower == null || upper == null) return true; //// nullチェック
        if (lower <= upper) return true;
        return false;
    }
    // 追記 ここまで

フォームに適当な日付(20250101)などを入れて確認してみましょう。
開始日が終了日以前でないと、「開始日は終了日以前を入力してください。」とエラーメッセージが表示されるはずです。

相関バリデーションを実装したい箇所に@AssertTrueアノテーションを付け、boolean型のメソッドを作成します(バリデーションの結果、正当であればtrue、そうでなければfalseを返す)。

また、このときメソッドはgetterメソッドとなるため、メソッド名は「is」もしくは「get」で始まる必要があります。view上では「is」以降の項目名(今回はdateValid)がエラー変数名となっています。

その他(発展)

以下の内容は発展として掲載します。必要に応じて活用ください。

ユニーク(重複、一意性)チェック

Springにはユニークチェックのバリデーションはありません。実装したい場合は自分で実装する必要があります。
Controller(Service)側で実装可能です。

参考サイト:
SpringBoot基礎 バリデーション編(入力チェック)- 3validationを自作する
4.2. 入力チェック — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.9.0.RELEASE documentation – 相関項目チェック

練習問題

問1: マニュアルで作成したフォームに対し、「Email」の入力項目を追加し、Emailバリデーションを追加してください。挑戦できる人は正規表現によるメールアドレスのバリデーションも検討しましょう。

タイトルとURLをコピーしました