0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UserRepositoryを作りながら、Spring Data JPAのRepository interfaceを理解した話

0
Posted at

はじめに

前回は、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は、最終的に次の形になります。

UserRepository.java
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型にしています。

User.java
@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

このLoginIdUser Entityのフィールド名に対応しています。

usersテーブルのカラム名は次のようにスネークケースです。

login_id

一方、JavaのUser Entityでは、次のようにキャメルケースで定義しています。

User.java
@Column(name = "login_id", nullable = false, length = 100)
private String loginId;

そのため、Repositoryのメソッド名はfindByLogin_idではなく、findByLoginIdになります。

ここで重要なのは、Spring Data JPAのメソッド名は、DBカラム名ではなくEntityのフィールド名をもとに解釈される点です。

UserRepository の責務

UserRepositoryによって、loginIdUserを検索できるようになりました。

ただし、この時点ではまだログイン判定は完成していません。
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のフィールド名をもとに検索条件を解釈する
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?