リポジトリという抽象
このレッスンでは、実装クラスを1行も書かずに保存と検索ができる Spring Data JPA のリポジトリの仕組みを説明できるようになります。
インターフェースを1つ書くだけです
Spring Data JPA でデータアクセスを用意するときに書くのは、次の3行だけです。
Java
public interface UserRepository extends JpaRepository<User, Long> {
}implements UserRepository と書いたクラスはどこにもありません。それでも userRepository.save(user) が動きます。起動時に Spring が、このインターフェースを実装したクラスを動的に生成し、DI コンテナへ登録しているからです。開発者から見ると「契約だけ宣言すれば、実装は用意される」という形になります。
型引数の2つは、扱うエンティティの型と、その主キーの型です。JpaRepository<User, Long> なら User を扱い、主キーは Long です。この2つが決まれば、後述する基本操作のシグネチャがすべて決まります。
継承するだけで手に入る操作
JpaRepository を継承すると、少なくとも次の操作がそのまま使えます。
| メソッド | 戻り値 | 役割 |
|---|---|---|
save(entity) | 保存されたエンティティ | 新規なら INSERT、既存なら UPDATE |
findById(id) | Optional<T> | 主キーで1件取得。無ければ空 |
findAll() | List<T> | 全件取得 |
deleteById(id) | なし | 主キーで1件削除 |
count() | long | 件数 |
existsById(id) | boolean | 存在確認 |
注目すべきは findById の戻り値です。User ではなく Optional<User> を返します。「見つからないかもしれない」という事実が型に現れているので、呼び出す側は必ずその場合を考えることになります。
Java
User user = userRepository.findById(1L)
.orElseThrow(() -> new NoSuchElementException("user not found"));save が新規と更新を兼ねている点も特徴です。渡されたエンティティの主キーが未設定なら新規として INSERT し、値が入っていて既に存在するなら UPDATE になります。呼び出し側は同じメソッドを呼ぶだけで済みます。
リポジトリは「保管庫」という考え方です
リポジトリという言葉はもともと、データの置き場所をコレクションのように扱う設計の呼び名です。save して findById で取り出す、という操作は、Map に put して get するのとほとんど同じ形をしています。SQL やコネクションの話は保管庫の中に閉じ込められ、外からは「入れる」「取り出す」だけが見えます。
この抽象が効いてくるのは差し替えのときです。テストでは本物のデータベースの代わりに、Map を使った軽い実装を差し込めます。同じインターフェースを実装している限り、呼び出す側のコードは1文字も変わりません。前の章で学んだ「インターフェースに依存する」という方針が、そのまま形になっています。
Java
public class InMemoryUserRepository implements UserRepository {
private final Map<Long, User> store = new HashMap<>();
// save と findById を Map の操作で実装する
}演習で書くもの
演習では、Spring が生成してくれるリポジトリの中身を自分で書きます。Map を保管庫にした疑似リポジトリをインターフェースと実装クラスの組で用意し、save してから findById した結果を返します。Spring では extends JpaRepository の1行で済むところを、ここでは保管庫が実際にやっている「主キーをキーにして入れる、キーで引く」という操作として書き下します。見つからなかったときにどう返すか、という Optional に相当する扱いも自分で決めます。
要件
- UserRepository インターフェースに save と findById を宣言し、InMemoryUserRepository が Map で実装すること
- run は保管庫を1つ作り、save してから findById した結果を返すこと
- 見つかったときは
found:に名前をつないだ文字列、見つからないときはemptyを返すこと
入出力例
run(1, "taro", 1) → "found:taro"
run(1, "taro", 2) → "empty"
run(99, "hanako", 99) → "found:hanako"
run(7, "guest", 0) → "empty"