C
Spring Boot/JPA/Lesson 08

JPA — *Javaオブジェクトでデータベースを操作する*

60分·theory
このチャプター
1/2

JPA — *Javaオブジェクトでデータベースを操作する*

🎯 このレッスンを読み終えたら

このレッスンを最後まで読み終えると、以下の3つを自信を持って実践できるようになります。

  • ✅ JpaRepositoryを継承するだけでコード0行でCRUDを実装
  • ✅ N+1問題 → @EntityGraphまたはJOIN FETCHで解決
  • ✅ LazyローディングとEagerローディングの違い + 実務では必ずLazyを選ぶ理由

学習目標をチェックリストとして手元に置き、すべてに答えられるようになったらレッスンを閉じてください。

JPAとは何か — *SQLを書かずにDBを使う*

要点を一言で

JPA (Java Persistence API) = JavaオブジェクトをDBテーブルに自動マッピングする標準仕様。SQLを直接書かず、Javaメソッドを呼ぶだけでCRUDが可能。

旧来の方法 — JDBCの苦労

java
// ユーザー1件取得
Connection conn = DriverManager.getConnection(...);
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setLong(1, 42L);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
    User u = new User();
    u.setId(rs.getLong("id"));
    u.setName(rs.getString("name"));
    u.setEmail(rs.getString("email"));
    // ... 30行
}
rs.close(); ps.close(); conn.close();

その都度接続管理・SQL記述・結果マッピングをすべて手動で行う必要があります。500テーブルあれば数万行にも及ぶ繰り返しコードに。

新しい方法 — JPAの魔法

java
@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private String email;
}

// 使用例
User u = entityManager.find(User.class, 42L);     // 1行
u.setName("名前変更");                              // setterを呼ぶだけでUPDATEが予約される
// トランザクション終了時に自動でUPDATE

SQLを一切書かずJavaオブジェクトだけを扱えば、DBが自動的に同期されます。「オブジェクトで作業し、DBは忘れる」 — JPAの核心思想。

HibernateとJPA

  • JPA標準仕様(インターフェース)。javax.persistenceパッケージ
  • Hibernate実装。最も人気(シェア95%超)。EclipseLink・OpenJPAも存在

Spring Data JPA = SpringがJPAをさらに使いやすくした抽象化レイヤー。実務の標準スタック。

Entityマッピング — オブジェクトとテーブルの対応付け

java
@Entity                              // JPAの管理対象
@Table(name = "users")                // マッピングするテーブル名
public class User {

    @Id                              // 主キー
    @GeneratedValue(strategy = IDENTITY)  // 自動採番
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(unique = true, nullable = false)
    private String email;

    @Enumerated(EnumType.STRING)      // enum → 文字列カラム
    private UserStatus status;

    @CreationTimestamp                 // 作成日時を自動セット
    private LocalDateTime createdAt;

    @UpdateTimestamp                   // 更新日時を自動セット
    private LocalDateTime updatedAt;
}

リレーションマッピング — 外部キーをオブジェクト参照に変換

java
@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne                        // 多対一(複数注文 → 1ユーザー)
    @JoinColumn(name = "user_id")     // FKカラム
    private User user;
}

@Entity
public class User {
    @OneToMany(mappedBy = "user")     // 一対多(1ユーザー → 複数注文)
    private List<Order> orders;
}

DBの外部キー関係Javaオブジェクト参照に変換。order.getUser().getName()のように自然にナビゲートできます。

永続化コンテキスト — JPAの秘密兵器

JPAは永続化コンテキスト(Persistence Context)という一次キャッシュを持ちます。1トランザクション内では同じエンティティのDB問い合わせは1回だけ

java
@Transactional
public void update(Long id) {
    User u1 = userRepo.findById(id).get();    // DB SELECT
    User u2 = userRepo.findById(id).get();    // キャッシュヒット、DBアクセスなし
    System.out.println(u1 == u2);              // true(同一オブジェクト)

    u1.setName("変更後");
    // 明示的な.save()不要 → トランザクション終了時に自動UPDATE
    // これが「ダーティチェッキング」
}

ダーティチェッキング(Dirty Checking) — JPAが開始時点と終了時点のオブジェクトを比較し、変更されたフィールドだけをUPDATE。setterを呼ぶだけで自動的に保存されます。

まとめ

  • JPA = Javaオブジェクト ↔ DBテーブルの自動マッピング標準
  • Hibernate = 最も人気の実装
  • Spring Data JPA = Springがさらに使いやすくした抽象化レイヤー
  • アノテーションでマッピング、リレーションはオブジェクト参照、変更は自動検知

Spring Data JpaRepository — *クエリの自動生成*

要点を一言で

JpaRepositoryインターフェースを定義するだけで基本的なCRUDメソッドが自動で作成されます。さらにメソッド名を見るだけでクエリを推測して自動生成してくれます。

基本CRUD — すぐに使える

java
public interface UserRepository extends JpaRepository<User, Long> {
}

この1行だけで以下のメソッドすべてが使えます:

java
userRepo.save(user);              // INSERTまたはUPDATE
userRepo.findById(42L);           // SELECT WHERE id=?
userRepo.findAll();               // SELECT *
userRepo.delete(user);            // DELETE
userRepo.count();                 // COUNT(*)
userRepo.existsById(42L);         // EXISTS

クエリを1行も書かずにフルCRUDが可能。まるで魔法のようです。

クエリメソッド — 名前がそのままクエリ

java
public interface UserRepository extends JpaRepository<User, Long> {
    // findBy + フィールド名 → 自動クエリ生成
    Optional<User> findByEmail(String email);
    // → SELECT * FROM users WHERE email = ?

    List<User> findByAgeGreaterThan(int age);
    // → SELECT * FROM users WHERE age > ?

    List<User> findByStatusAndCreatedAtAfter(Status s, LocalDateTime t);
    // → SELECT * FROM users WHERE status = ? AND created_at > ?

    boolean existsByEmail(String email);
    long countByStatus(Status s);
}

ルール: findexistscountdeleteで始まり + By + フィールド名・条件キーワード。Springがパースしてクエリを生成してくれます。

@Query — 複雑なクエリ

命名規則で表現できない場合は直接記述:

java
public interface UserRepository extends JpaRepository<User, Long> {

    @Query("SELECT u FROM User u WHERE u.email LIKE %:keyword% OR u.name LIKE %:keyword%")
    List<User> search(@Param("keyword") String keyword);

    @Modifying       // INSERT・UPDATE・DELETEに必須
    @Query("UPDATE User u SET u.status = :status WHERE u.lastLogin < :date")
    int deactivateInactive(@Param("status") Status status, @Param("date") LocalDateTime date);
}

JPQL(JPA Query Language)— SQLに似ていますがテーブル名ではなくエンティティ名を使用。DB非依存です。

ネイティブSQLも利用可能:

java
@Query(value = "SELECT * FROM users WHERE ...", nativeQuery = true)

QueryDSL — 動的クエリの標準

@Queryにも限界があります — 条件が動的な場合(検索フィルターなど)はQueryDSLの出番:

java
QUser u = QUser.user;
BooleanBuilder where = new BooleanBuilder();
if (keyword != null) where.and(u.name.contains(keyword));
if (status != null)  where.and(u.status.eq(status));

List<User> result = queryFactory.selectFrom(u).where(where).fetch();

型安全でコンパイル時に検証。Spring Data JPAとともに実務の標準です。

ページング — 自動サポート

java
Page<User> page = userRepo.findByStatus(Status.ACTIVE, PageRequest.of(0, 20));

page.getContent();          // 現在ページの結果
page.getTotalElements();    // 全件数
page.getTotalPages();        // 全ページ数

Pageableを引数に受け取るだけでLIMIT・OFFSET処理が自動。ページング情報も自動で返されます。

まとめ

  • JpaRepositoryを継承するだけで基本CRUDが無料
  • メソッド名でクエリが自動生成
  • 複雑な場合は@QueryまたはQueryDSL
  • ページング・ソートもPageableの1行で完結

N+1問題 — *JPAで最もよくある落とし穴*

問題の状況

java
@Transactional
public void printAllOrders() {
    List<User> users = userRepo.findAll();      // 1回: SELECT * FROM users
    for (User u : users) {
        System.out.println(u.getOrders().size());  // N回: SELECT * FROM orders WHERE user_id = ?
    }
}

ユーザーが100人なら合計101回のクエリ。1万人なら1万1回。ページのレスポンスタイムが5秒から50秒へと急増します。

これがN+1問題。JPAの遅延ローディング(Lazy Loading)が意図と異なる動作をする典型的な落とし穴です。

なぜ発生するのか

JPAは関連オブジェクトをデフォルトでLazyにロードします。user.getOrders()初めて呼び出した瞬間に追加クエリが発行されます。一度にすべて取得すると不要なデータが多くなる可能性があるため、必要になったときだけ取得する設計です。

ただしループ内で呼び出すと毎回新しいクエリが → N+1。

解決策1 — Fetch Join

java
@Query("SELECT u FROM User u JOIN FETCH u.orders")
List<User> findAllWithOrders();

JOINで一度にすべて取得します。クエリ1回で完了。

生成されるSQL:

sql
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

解決策2 — @EntityGraph

java
public interface UserRepository extends JpaRepository<User, Long> {
    @EntityGraph(attributePaths = "orders")
    List<User> findAll();
}

特定のメソッドにのみEagerローディングを適用。Fetch Joinよりもスッキリ書けます。

解決策3 — @BatchSize

関連データをN+1ではなくバッチ単位でまとめて取得します。

java
@Entity
public class User {
    @OneToMany
    @BatchSize(size = 100)
    private List<Order> orders;
}

ユーザー1000人 → 10クエリ(100件ずつ)。完全な解決ではありませんが大幅な改善になります。

コレクション2つのFetch Join — 危険

java
@Query("SELECT u FROM User u JOIN FETCH u.orders JOIN FETCH u.reviews")
List<User> findAllFull();
// MultipleBagFetchException 発生!

コレクションを2つ同時にFetch Joinするとデータがデカルト積(M × N)で爆発します。対策:

  • 1つだけFetch Join + 残りはBatchSize
  • またはDTOプロジェクションで必要なフィールドだけ取得

DTOプロジェクション — さらなる最適化

エンティティ全体ではなく必要なフィールドだけ取得:

java
public interface UserSummary {
    Long getId();
    String getName();
    Long getOrderCount();
}

@Query("SELECT u.id AS id, u.name AS name, COUNT(o) AS orderCount " +
       "FROM User u LEFT JOIN u.orders o " +
       "GROUP BY u.id, u.name")
List<UserSummary> findSummaries();

エンティティマッピングとLazyローディングを完全に回避。最も高速な選択肢です。

診断 — ログで確認

yaml
spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true

またはより強力なP6Spy — 実際のパラメータ値まで表示してくれます。すべてのクエリを目で確認しながら、N+1の発生を即座に検知できます。

まとめ

  • N+1はLazyローディング + ループの組み合わせが原因
  • 解決策: Fetch Join > @EntityGraph > @BatchSize > DTOプロジェクション
  • show-sqlで常にクエリをトレース
  • コレクション2つのFetch Joinは危険 — 1つに留める

🤖 AIへの質問例

このレッスンの概念を理解すれば、AIに具体的な指示を出せるようになります。漠然とした「直して」ではなく、語彙を持ったリクエスト — それがトークン節約の出発点です。

  • 「このfindAll + ループがN+1になっているので@EntityGraphで解決して」
  • 「このEntityをUserResponse DTOに変換するstaticファクトリメソッドを追加して」
  • 「JpaRepositoryにfindByEmailAndActiveメソッドのシグネチャを追加して」

なぜトークンが減るのか

概念を知らないままだと、AIの回答を受け取っても「それは何ですか?」と再度質問しなければなりません。その「再質問」がトークンを消費します。概念を一度理解しておけば、会話が一度で完結します。

JPA — JavaオブジェクトでDBを扱う - Spring Boot