【Spring Boot】自作のバリデーションを作る

バリデーションを自作する方法を学んだので、備忘として本記事を作成しました。

# 使用環境
- Windows10 (64bit)
- spring-boot : 2.7.1
- postgres : 11.15

# ユースケース
ユーザーを新規会員登録させるときに、emailとpasswordの入力が必須。
その際に既に登録してあるemailで新規会員登録できないようにしたいので、
入力されたemailが既にDBに登録があった場合はエラーとなるように、バリデーションを自作する。
バリデーションの名前は``@UniqueEmail``とする。

## エンティティクラスの作成
今回、ユーザー情報はusersテーブルに格納しておくので、
そのテーブルに対応する``user``クラスを作成する。
~~~java:user.java
package com.example.domain;

import文省略

public class User {
	private int id;
	private String email;
	private String password;

    constructor,getter,setter 省略
}

~~~

## リポジトリクラスの作成
emailが既に登録されているかどうかDBに問い合わせるリポジトリクラス``UserRepository.java``を作成する。
今回は、Spring JDBCを用いてDBにアクセスする。
~~~java:UserRepository.java
package com.example.repository;

import文省略

@Repository
public class UserRepository {
	
	@Autowired
	private NamedParameterJdbcTemplate template;
	
	private static final RowMapper<User> USER_ROW_MAPPER = (rs, i) ->{
		User user = new User();
		user.setId(rs.getInt("id"));
		user.setEmail(rs.getString("email"));
		user.setPassword(rs.getString("password"));	
		return user;
	};
	
	/**
	 * emailからユーザーを特定する
	 * @param email
	 * @return 
	 * 既に登録があった場合：emailを返却
	 * 登録がなかった場合：EmptyResultDataAccessExceptionをスロー
	 */
	public String findByEmail(String email) throws EmptyResultDataAccessException{
		String sql = "SELECT email FROM users WHERE email = :email";
		SqlParameterSource param = new MapSqlParameterSource().addValue("email", email);
		return template.queryForObject(sql, param, USER_ROW_MAPPER);
	}
~~~

## UniqueEmailクラスを作成
本件の肝です。
今回、バリデーションの名前を``@UniqueEmail``にしたいので、``UniqueEmail``クラスを作成します。

### ✓ポイント
- クラスの宣言に、``@interface`` と付ける
- クラスに下記アノテーションをつける
  - ``@Target`` ： アノテーションが付与できる場所の指定を行う
  - ``@Retention``：いつまでアノテーションを残すか設定する
  - ``@Constraint``：バリデーションを実行するクラスを指定する

~~~java:UniqueEmail.java
package com.example.validation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ ElementType.METHOD, ElementType.FIELD }) // アノテーションが付与できる場所の指定。今回は、メソッドとフィールドにアノテーションを付与できると指定。
@Retention(RetentionPolicy.RUNTIME) // いつまでアノテーションを残すか。この記述はRuntimeは実行時まで残すということ。
@Constraint(validatedBy = UniqueEmailValidator.class) // バリデーションを実行するクラスを指定。今回はUniqueEmailValidatorクラス で使用する。UniqueEmailValidatorクラスは後程実装する。
public @interface UniqueEmail { // クラスの宣言に、@interface と付ける

    // バリデーションが働いた時のエラーメッセージをここで指定する。
	String message() default "このEmailは既に使われています。別のメールアドレスを入力してください。";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};
}

~~~


## バリデータークラスを作成
本件の肝2です。

### ✓ポイント
- ``ConstraintValidator``インターフェイスをimplementsして、``isValid``メソッドを実装する

~~~java:UniqueEmailValidator.java
package com.example.validation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.springframework.beans.factory.annotation.Autowired;

import com.example.repository.UserRepository;

// ConstraintValidator<A,T>インターフェイスをimplementsする。Aには先ほど作成したUniqueEmailクラスを指定する。
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String>{
	
	@Autowired
	private UserRepository userRepository;

    /**
	 * isValidメソッドを実装し、このなかでバリデーションエラー判定を行う
	 * @return 
	 * emailに重複がなかった場合：true
	 * emailに重複があった場合：false
	 */
	@Override
	public boolean isValid(String value, ConstraintValidatorContext context) {
		try {
			userRepository.findByEmail(value);
			return false;
		} catch (Exception e) {
			return true;
		}
	}

}

~~~

## フォームクラスを作成する
ユーザーからの入力値を受け取るフォームクラス``UserForm``を作成する。
先ほど作成した``@UniqueEmail``をここで使用する。

~~~java:UserForm.java
package com.example.form;

import com.example.validation.UniqueEmail;

public class UserForm {
	
	@UniqueEmail // 自作のバリデーターを使えるようになる
	private String email;
	private String password;

    constructor,getter,setter 省略

}

~~~

# 終わりに
自作のバリデーターを使いたくなる場面は結構出てくるかと思うので、備忘のために執筆しました。
間違っている部分もあるかと思いますので、その際はご指摘頂ければと思います。

# 参考文献
[【Spring Boot】バリデーション](https://b1san-blog.com/post/spring/spring-validation/)
[Spring Frameworkで使用できるBean Validationアノテーション一覧](https://penguinlabo.hatenablog.com/entry/springframework/validator)