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

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

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

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

#アノテーションを自作する
まず、下記の画像のように`com.example.demo`上で右クリックをし、新規→注釈の順でアノテーションインターフェースの雛形を作成する。
![スクリーンショット 2021-11-16 6.38.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/879778/46c9b959-f78c-a5a1-dd2b-b4fa80a0bae5.png)

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

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

##バリデーションクラス
```java
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カラムの値を取得するようにしている。

```java
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に登録されているメールアドレスを渡すことができる。

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

}
```

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

```java
@Unused
private String email;
```

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

実際に以下の`usersテーブル`に登録されている値を入力してみて確認してみた。
![スクリーンショット 2021-11-16 7.02.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/879778/f846caae-3432-2dcb-fc5e-5952f70a63ac.png)

以下の画像にあるように、しっかりアノテーションが実装されていることが確認できた。
![スクリーンショット 2021-11-16 7.01.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/879778/0a70e936-1931-0b4a-15b4-d570494c417b.png)

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

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

#参考文献
・[Spring BootでBean Validation (3) バリデーション処理を自作](https://ti-tomo-knowledge.hatenablog.com/entry/2018/06/18/094526)

・[Spring Boot 自作アノテーションを作成して複数項目にvalidationをかける](https://qiita.com/daifuku_mochi2/items/c90cab977f0f37b30dbf)

・[Spring Data JPA でのクエリー実装方法まとめ](https://qiita.com/tag1216/items/55742fdb442e5617f727)