はじめに
Spring Data JPAを使うと、こんなコードだけでCRUD操作ができます。
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
実装クラスを一切書いていないのに、なぜ動くのか?
この記事では、Spring Data JPAが裏側で何をしているかを、初学者でも理解できるように解説します。
前提知識:レイヤー構造
Spring Bootの典型的な構造です。
Controller(リクエスト受付)
↓
Service(ビジネスロジック)
↓
Repository(DB操作) ← 今回の主役
↓
Database
RepositoryはDBへのアクセスを担当する層です。
素のJPA(EntityManager)でCRUDを書くとどうなるか
Spring Data JPAを使わずに、JPA本体のEntityManagerで書いた場合です。
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Repository
@Transactional
public class TodoRepositoryImpl {
@PersistenceContext
private EntityManager em;
// 全件取得
public List<Todo> findAll() {
return em.createQuery("SELECT t FROM Todo t", Todo.class)
.getResultList();
}
// 1件取得
public Optional<Todo> findById(Long id) {
Todo todo = em.find(Todo.class, id);
return Optional.ofNullable(todo);
}
// 作成・更新
public Todo save(Todo todo) {
if (todo.getId() == null) {
em.persist(todo); // INSERT
return todo;
} else {
return em.merge(todo); // UPDATE
}
}
// 削除
public void deleteById(Long id) {
Todo todo = em.find(Todo.class, id);
if (todo != null) {
em.remove(todo);
}
}
}
findAll、findById、save、deleteById...これはどのエンティティでもほぼ同じコードです。
このボイラープレートを自動化したのがSpring Data JPAです。
JpaRepositoryの継承階層
JpaRepositoryは複数のインターフェースを継承しています。
Repository(マーカーインターフェース)
└─ CrudRepository(基本CRUD)
└─ ListCrudRepository(戻り値がList版)
└─ JpaRepository(JPA固有の機能を追加)
CrudRepositoryに定義されているメソッド
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity); // 1件保存
<S extends T> Iterable<S> saveAll(...); // 複数件保存
Optional<T> findById(ID id); // IDで1件取得
boolean existsById(ID id); // 存在確認
Iterable<T> findAll(); // 全件取得
long count(); // 件数取得
void deleteById(ID id); // IDで削除
void delete(T entity); // エンティティで削除
void deleteAll(); // 全件削除
}
これらのメソッドが定義だけされています。実装はどこにあるのでしょうか?
裏側の仕組み:4ステップで理解する
ステップ1:Springがインターフェースをスキャンする
アプリケーション起動時に、Springはクラスパス上のインターフェースをスキャンします。
// これを見つける
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
Repository(またはそのサブインターフェース)を継承しているインターフェースが対象です。
ステップ2:プロキシ(代理オブジェクト)を動的に生成する
Springは見つけたインターフェースごとに、実装クラスを実行時に動的に生成します。これを「プロキシ」と呼びます。
あなたが書くもの:
TodoRepository(インターフェース ← 中身は空)
Springが自動生成するもの:
TodoRepositoryのプロキシ(実装クラス)
Javaのjava.lang.reflect.Proxyという仕組みを使っています。
ステップ3:SimpleJpaRepositoryが実装を提供する
生成されたプロキシの中身は、**SimpleJpaRepository**というクラスが担当しています。
// Spring Data JPAの内部実装(簡略化)
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID> {
private final EntityManager em;
private final Class<T> entityClass; // 例: Todo.class
// 全件取得
@Override
public List<T> findAll() {
return em.createQuery("SELECT e FROM " + entityName + " e", entityClass)
.getResultList();
}
// 1件取得
@Override
public Optional<T> findById(ID id) {
T entity = em.find(entityClass, id);
return Optional.ofNullable(entity);
}
// 保存(新規 or 更新を自動判定)
@Override
public <S extends T> S save(S entity) {
if (isNew(entity)) {
em.persist(entity); // INSERT
return entity;
} else {
return em.merge(entity); // UPDATE
}
}
// 削除
@Override
public void deleteById(ID id) {
T entity = findById(id).orElseThrow(() ->
new EmptyResultDataAccessException(1));
em.remove(entity);
}
}
先ほど手書きしたコードとほぼ同じです。Spring Data JPAがこれを代わりにやってくれています。
ステップ4:EntityManagerがSQLを生成・実行する
SimpleJpaRepositoryは内部でEntityManagerを使います。EntityManagerがエンティティの情報からSQLを生成します。
todoRepository.findAll()
↓
SimpleJpaRepository.findAll()
↓
EntityManager.createQuery("SELECT t FROM Todo t")
↓
Hibernate(JPA実装)がSQLに変換
→ SELECT id, title, completed FROM todo
↓
JDBC でDBに問い合わせ
↓
結果を Todo オブジェクトにマッピングして返す
全体像
TodoRepository (インターフェース)
│ あなたが書く。extends JpaRepository<Todo, Long> するだけ
↓
Spring がプロキシを自動生成
│ 起動時に TodoRepository の実装クラスを動的に作る
↓
SimpleJpaRepository (Spring Data JPAが提供する実装)
│ findAll(), save(), deleteById() 等の中身がここにある
↓
EntityManager (JPA本体)
│ エンティティ情報からJPQL(Javaのクエリ言語)を生成
↓
Hibernate (JPAの実装ライブラリ)
│ JPQLを実際のSQLに変換
↓
JDBC → Database
| 層 | 誰が担当 | やること |
|---|---|---|
| Repository | あなた | 「何がしたいか」を宣言 |
| プロキシ | Spring | インターフェースと実装をつなぐ |
| SimpleJpaRepository | Spring Data JPA | CRUDの実装 |
| EntityManager | JPA | JPQL生成 |
| Hibernate | ORM | SQL変換・実行 |
| JDBC | Java標準 | DB通信 |
カスタムクエリ:メソッド名からSQLを自動生成
Spring Data JPAは、メソッド名を解析してクエリを自動生成する機能も持っています。
public interface TodoRepository extends JpaRepository<Todo, Long> {
// メソッド名から自動でクエリが生成される
List<Todo> findByCompleted(boolean completed);
// → SELECT t FROM Todo t WHERE t.completed = ?
List<Todo> findByTitleContaining(String keyword);
// → SELECT t FROM Todo t WHERE t.title LIKE '%keyword%'
List<Todo> findByCompletedAndTitleContaining(boolean completed, String keyword);
// → SELECT t FROM Todo t WHERE t.completed = ? AND t.title LIKE '%keyword%'
}
実装は不要です。Springがメソッド名を解析して、適切なクエリを生成してくれます。
メソッド名のルール
| キーワード | 生成されるSQL | 例 |
|---|---|---|
findBy |
WHERE句 | findByTitle(String title) |
And |
AND | findByTitleAndCompleted(...) |
Or |
OR | findByTitleOrCompleted(...) |
Containing |
LIKE '%...%' | findByTitleContaining(String keyword) |
StartingWith |
LIKE '...%' | findByTitleStartingWith(...) |
OrderBy |
ORDER BY | findByCompletedOrderByIdDesc(...) |
CountBy |
COUNT | countByCompleted(boolean completed) |
DeleteBy |
DELETE | deleteByCompleted(boolean completed) |
@Queryで直接書くことも可能
複雑なクエリはメソッド名では表現しきれないので、@Queryで直接書けます。
public interface TodoRepository extends JpaRepository<Todo, Long> {
@Query("SELECT t FROM Todo t WHERE t.title LIKE %:keyword% AND t.completed = false")
List<Todo> searchActiveTodos(@Param("keyword") String keyword);
}
確認方法:実際に生成されるSQLを見る
application.propertiesに以下を追加すると、実行されるSQLがコンソールに表示されます。
# SQLを表示
spring.jpa.show-sql=true
# SQLを整形して表示
spring.jpa.properties.hibernate.format_sql=true
Hibernate:
select
t1_0.id,
t1_0.completed,
t1_0.title
from
todo t1_0
学習中はこれを有効にして、どんなSQLが実行されているか確認すると理解が深まります。
まとめ
| 疑問 | 答え |
|---|---|
| なぜインターフェースだけで動く? | Springが起動時にプロキシ(実装クラス)を自動生成するから |
| 実装はどこにある? |
SimpleJpaRepositoryというSpring Data JPA内部のクラス |
| SQLは誰が書いている? | EntityManager → Hibernate が自動生成 |
| カスタムクエリは? | メソッド名の命名規則か@Queryアノテーション |
| この仕組みはTodo専用? | いいえ。JpaRepositoryを継承すればどのエンティティでも同じ |
「インターフェースを宣言するだけで実装はフレームワークが自動生成する」——これがSpring Data JPAの設計思想であり、SQLを一行も書かずにDB操作できる理由です。
演習問題
問題1 ⭐(基本)
以下のBookエンティティに対応するRepositoryインターフェースを作成してください。
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private int price;
}
模範解答
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, Long> {
}
JpaRepository<エンティティの型, 主キーの型>を指定するだけです。これでsave(), findById(), findAll(), deleteById()等がすべて使えます。
問題2 ⭐⭐(応用)
BookRepositoryに以下のカスタムクエリメソッドを追加してください。メソッド名の命名規則を使い、@Queryは使わないこと。
- 著者名で検索する
- 価格が指定値以下の本を検索する
- タイトルに特定の文字列を含む本を、価格の昇順で検索する
模範解答
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface BookRepository extends JpaRepository<Book, Long> {
// 1. 著者名で検索
List<Book> findByAuthor(String author);
// → SELECT b FROM Book b WHERE b.author = ?
// 2. 価格が指定値以下
List<Book> findByPriceLessThanEqual(int price);
// → SELECT b FROM Book b WHERE b.price <= ?
// 3. タイトル部分一致 + 価格昇順
List<Book> findByTitleContainingOrderByPriceAsc(String keyword);
// → SELECT b FROM Book b WHERE b.title LIKE '%keyword%' ORDER BY b.price ASC
}
メソッド名をfindBy + フィールド名 + 条件キーワードの形式で書くと、Spring Data JPAが自動でクエリを生成します。
問題3 ⭐⭐⭐(チャレンジ)
以下のコードは、Spring Data JPAを使わずにEntityManagerで書いたRepositoryです。これをSpring Data JPAのJpaRepositoryを使って書き換え、同じ機能を実現してください。
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Repository
@Transactional
public class BookRepositoryImpl {
@PersistenceContext
private EntityManager em;
public List<Book> findAll() {
return em.createQuery("SELECT b FROM Book b", Book.class)
.getResultList();
}
public Book findById(Long id) {
return em.find(Book.class, id);
}
public Book save(Book book) {
if (book.getId() == null) {
em.persist(book);
return book;
} else {
return em.merge(book);
}
}
public void deleteById(Long id) {
Book book = em.find(Book.class, id);
if (book != null) {
em.remove(book);
}
}
public List<Book> findByAuthor(String author) {
return em.createQuery("SELECT b FROM Book b WHERE b.author = :author", Book.class)
.setParameter("author", author)
.getResultList();
}
public List<Book> findCheapBooks(int maxPrice) {
return em.createQuery("SELECT b FROM Book b WHERE b.price <= :maxPrice ORDER BY b.price ASC", Book.class)
.setParameter("maxPrice", maxPrice)
.getResultList();
}
}
模範解答
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface BookRepository extends JpaRepository<Book, Long> {
// findAll(), findById(), save(), deleteById() は JpaRepository に含まれるので不要
// 著者名で検索
List<Book> findByAuthor(String author);
// 価格以下を昇順で取得
List<Book> findByPriceLessThanEqualOrderByPriceAsc(int maxPrice);
}
40行以上のコードが、インターフェース定義だけになりました。
-
findAll(),findById(),save(),deleteById():JpaRepositoryに含まれるため宣言不要 -
findByAuthor():メソッド名の命名規則で自動生成 -
findCheapBooks():findByPriceLessThanEqualOrderByPriceAscに名前を変えることで自動生成
これがSpring Data JPAの力です。SimpleJpaRepositoryとメソッド名解析が、ボイラープレートコードをすべて自動化してくれます。
参考
- https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html
- https://docs.spring.io/spring-data/jpa/reference/repositories/query-keywords-reference.html
- https://github.com/spring-projects/spring-data-jpa/blob/main/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!