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?

【Spring Boot】JpaRepositoryの裏側 〜 なぜインターフェースだけでDBが操作できるのか

0
Posted at

はじめに

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

findAllfindByIdsavedeleteById...これはどのエンティティでもほぼ同じコードです。

このボイラープレートを自動化したのが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は使わないこと。

  1. 著者名で検索する
  2. 価格が指定値以下の本を検索する
  3. タイトルに特定の文字列を含む本を、価格の昇順で検索する
模範解答
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とメソッド名解析が、ボイラープレートコードをすべて自動化してくれます。

参考


@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!

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?