はじめに
前回は、DBのusersテーブルに対応するUser Entityを作成しました。
今回はその続きとして、loginIdを使ってDB上のuserを検索するために、UserRepositoryを作成しました。
最初は、Repositoryと聞いても「DBにアクセスするクラス」くらいの理解でした。
しかし実際に書いてみると、interfaceとして定義する理由やジェネリクスの指定など、想定より分からない点が多く出てきました。
この記事では、UserRepositoryを作りながら、Spring Data JPAのRepositoryまわりで詰まったことを整理します。
今回やろうとしていたこと
ログイン処理の中で、入力されたloginIdに対応するuserをDBから探すことが今回の目的です。
ログインAPIでは、利用者が次のような情報を送ります。
{
"loginId": "test_taro",
"password": "password_taro"
}
このうち、まず必要になるのがloginIdによるuser検索です。
流れとしては、次のようなイメージです。
今回の記事では、password照合までは扱いません。
まずは、loginIdでuserを検索できるRepositoryを作るところまでを対象にします。
今回作るRepositoryの全体像
今回作成するUserRepositoryは、最終的に次の形になります。
package com.yoshida.orgflow.repository;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.yoshida.orgflow.entity.User;
@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
Optional<User> findByLoginId(String loginId);
}
コードは短いものの、自分にとっては短いわりに分からないことが多いコードでした。
特に詰まったのは、次の4点です。
1. なぜ class ではなく interface なのか
2. なぜ JpaRepository を継承するのか
3. <User, UUID> は何を指定しているのか
4. findByLoginId というメソッド名にどんな意味があるのか
最初に全体像を置いたうえで、それぞれの疑問を順番に整理していきます。
最初の疑問:なぜRepositoryをinterfaceで作るのか
最初に引っかかったのは、Repositoryをinterfaceとして作る点でした。
public interface UserRepository extends JpaRepository<User, UUID> {
Optional<User> findByLoginId(String loginId);
}
自分の中では、interfaceは次のようなものだと理解していました。
interfaceに抽象メソッドを書き、それを実装するclass側で具体的な処理を書くもの
その理解自体は大きく間違っていないと思います。
ただ、今回のUserRepositoryでは、自分で実装クラスを書いていません。
では、実際の処理は誰が作るのか?
ここで出てくるのがSpring Data JPAです。
Spring Data JPAでは、JpaRepositoryを継承したRepository interfaceを定義すると、そのinterfaceをもとにRepositoryの実装を用意してくれます。
今回のUserRepositoryは、自分でSQLを書くためのclassではなく、「Userに対してこういうDB操作をしたい」とSpring Data JPAに伝えるためのinterfaceです。
自分で実装クラスを書かなくても、Spring Data JPAがRepositoryとして使えるようにしてくれます。
JpaRepository<User, UUID> の意味
次に分からなかったのが、次の部分です。
extends JpaRepository<User, UUID>
JpaRepositoryを継承しているのは、基本的なDB操作を使えるようにするためです。
JpaRepositoryには次のような操作が用意されています。
findById
findAll
save
deleteById
existsById
今回すぐに全部を使うわけではありません。
しかし、User EntityをDBアクセス対象として扱うRepositoryにするため、JpaRepositoryを継承します。
<User, UUID> は何を表しているのか
次に気になったのが、<User, UUID>の部分です。
JpaRepository<User, UUID>
これはJavaのジェネリクスで、JpaRepositoryに対して「何を扱うRepositoryなのか」を指定しています。
今回の場合は次の意味になります。
User: このRepositoryで扱うEntity
UUID: User Entityの主キーの型
User Entityの主キーは、次のようにUUID型にしています。
@Id
@Column(name = "id")
private UUID id;
そのため、Repository側もJpaRepository<User, UUID>になります。
もしここをJpaRepository<User, String>のように書いてしまうと、Userの主キーはStringである、とSpring Data JPAに伝えてしまうことになります。
<User, UUID>はただのおまじないではなく、Repositoryが扱うEntityと、その主キーの型を指定しているものです。
Optional<User> の意味
次に出てきた疑問が、Optional<User>です。
Optional<User> findByLoginId(String loginId);
最初は、なぜ戻り値指定にOptional<User>を使うのかが曖昧でした。
Optional<User>は、Userが存在するかもしれないし、存在しないかもしれないことを型で表しているものです。
より正確には、次の意味です。
loginIdに一致するUserが見つかった
→ Optionalの中にUserオブジェクトが入る
loginIdに一致するUserが見つからなかった
→ Optional.empty()
なぜUserをそのまま返さないのか
戻り値を次のようにした場合を考えます。
User findByLoginId(String loginId);
この場合、該当するuserが見つからなかったときに、nullが返る可能性があります。
すると、呼び出し側では次のような注意が必要になります。
User user = userRepository.findByLoginId(loginId);
if (user == null) {
// 見つからなかった場合の処理
}
nullはチェックを忘れやすく、後からNullPointerExceptionの原因になることがあります。
一方、Optional<User>にしておけば、呼び出し側に対して「この検索結果は存在しない可能性がある」ということを型で伝えられます。
ログイン処理ではloginIdに一致するuserが存在しないことは十分ありえます。
そのため、Optional<User>として返すのが自然だと理解しました。
findByLoginId は自由な名前ではなかった
次に疑問だったのが、findByLoginIdというメソッド名です。
Optional<User> findByLoginId(String loginId);
最初は、分かりやすければ任意の名前でよいのかと思っていました。
しかし、Spring Data JPAではこのメソッド名に意味があります。
findByLoginIdは、次のように分解できます。
findBy + LoginId
このLoginIdはUser Entityのフィールド名に対応しています。
usersテーブルのカラム名は次のようにスネークケースです。
login_id
一方、JavaのUser Entityでは、次のようにキャメルケースで定義しています。
@Column(name = "login_id", nullable = false, length = 100)
private String loginId;
そのため、Repositoryのメソッド名はfindByLogin_idではなく、findByLoginIdになります。
ここで重要なのは、Spring Data JPAのメソッド名は、DBカラム名ではなくEntityのフィールド名をもとに解釈される点です。
UserRepository の責務
UserRepositoryによって、loginIdでUserを検索できるようになりました。
ただし、この時点ではまだログイン判定は完成していません。
UserRepositoryの責務は、あくまでDB上のuserを探すことです。
passwordが合っているかどうかを判断するのは、次に作るAuthServiceの責務です。
今回理解したこと
UserRepositoryを作りながら、Spring Data JPAのRepositoryについて次のことが整理できました。
-
UserRepositoryは、自分でSQLや実装クラスを書くためのclassではなく、Spring Data JPAに「Userに対してこういうDB操作をしたい」と伝えるためのinterfaceである -
JpaRepository<User, UUID>は、扱うEntityと主キーの型を指定するジェネリクスである -
Optional<User>は、検索結果が存在しない可能性を型で表すために使う -
findByLoginIdのメソッド名は自由ではなく、Spring Data JPAがEntityのフィールド名をもとに検索条件を解釈する