😀

SpringBootで一意性制約のアノテーションを自作(コピペで完成)

2021/11/16に公開

#はじめに
SpringBootでアプリケーションを開発中に、Emailやその他ユニークな値の制約の設定の仕方がわからず、ググったところ、Springのバリデーションでは一意性のエラーチェックがないようなので、自作することになった。

ところが、自分が参考にしている記事を超える記事がなく、小1時間ほど悩んでようやく実装できたので、備忘録として残すことにした。
※Railsならdeviceが自動で実装してくれていた...

#環境
・Java11
・SpringBoot2.5.6
・Spring Data JPA
・MySQL
・gradle

#アノテーションを自作する
まず、下記の画像のようにcom.example.demo上で右クリックをし、新規→注釈の順でアノテーションインターフェースの雛形を作成する。
スクリーンショット 2021-11-16 6.38.02.png

次に、実際にアノテーションインターフェースのコードを書いていく。

##アノテーションインターフェース

package パッケージ名;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

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

@Documented
@Constraint(validatedBy = UnusedValidator.class) //この後に作成するバリデーションクラスを指定する
@Target({FIELD}) // 項目に対してバリデーションをかける場合はFIELDを選ぶ
@Retention(RUNTIME)
public @interface Unused {

	String message() default "すでに登録済みのメールアドレスです"; // エラーメッセージ
	 
    Class<?>[] groups() default {};
 
    Class<? extends Payload>[] payload() default {};
 
    @Target({FIELD})
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        Unused[] value(); // インターフェース名[] value()とする
    }

}

Class<?>[]・・・などよくわからない記述も多いと思うが、アノテーションインターフェースはほぼほぼ定型のようなものなので、ここでの解説は割愛する。

##バリデーションクラス

package com.example.demo.validate;

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

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

import com.example.demo.entity.MUser;
import com.example.demo.service.UserService;

public class UnusedValidator implements ConstraintValidator<Unused, String> {

	@Autowired
    private UserService service;//各自で設定が必要

    public void initialize(Unused constraintAnnotation) {
    }

    public boolean isValid(String value, ConstraintValidatorContext context) {
    	//各自で設定が必要
    	MUser email = service.findByEmail(value).orElse(null); // ここのvalueは入力値
        if(email == null){
        	return true;
        } 

        return false;
    }
}

バリデーションクラスではインターフェースをimplementsしてアノテーションの内容を記述していく。

アノテーションの処理内容としては、boolean型isValid()メソッドがアノテーションに入る。
ここで、ServiceクラスのfindByEmail()を呼び出し、条件分岐により、処理内容を分けている。

##Repositoryクラス
実際の、findByEmail()メソッドの中身だが、JPAを利用して、DBからemailカラムの値を取得するようにしている。

package com.example.demo.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.demo.entity.MUser;

@Repository
public interface UserRepository extends JpaRepository<MUser, Integer> {

	 public Optional<MUser> findByEmail(String email);
}

##Serviceクラス
そして、ServiceクラスでfindByEmail()メソッドを定義しRepositoryのメソッドを返すようにする。

これにより、アノテーションのバリデーションクラスにDBに登録されているメールアドレスを渡すことができる。

package com.example.demo.service;

import java.util.Optional;

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

import com.example.demo.entity.MUser;
import com.example.demo.form.SignupForm;
import com.example.demo.repository.UserRepository;

@Service
public class UserService {
	
	@Autowired
	private UserRepository userRepository;

	/**メールアドレスでユーザー検索*/
	public Optional<MUser> findByEmail(String email) {		
		return userRepository.findByEmail(email);
	}

}

##実装
これで後は@〇〇のようにアノテーションを実装するだけで完了だ。

@Unused
private String email;

各自、EntityなりFormなりバリデーションを実装しているクラスがあると思うので、そこで、実装するだけで完了だ。

実際に以下のusersテーブルに登録されている値を入力してみて確認してみた。
スクリーンショット 2021-11-16 7.02.33.png

以下の画像にあるように、しっかりアノテーションが実装されていることが確認できた。
スクリーンショット 2021-11-16 7.01.49.png

筆者のように「一意性制約」などピンポイントでの実装をググる方は結構いると思うが、どの記事も、アノテーションの実装の内容は理解できるが、実際にサービスクラスの中身等まで解説しているのはなく、「各自で実装しろ」と言われても・・・という状態になると思う。

筆者もそうだったため、参考になれば幸いだ。

#参考文献
Spring BootでBean Validation (3) バリデーション処理を自作

Spring Boot 自作アノテーションを作成して複数項目にvalidationをかける

Spring Data JPA でのクエリー実装方法まとめ

Discussion