Spring Data JPAとは、Repositoryインターフェースのメソッド名や@Queryからデータベース検索を自動で組み立ててくれる、SpringのJPA向けデータアクセス機能です。 findByStatusAndAgeGreaterThan のように決まったキーワードでメソッド名を書くと、実装クラスを書かなくてもJPQL(JPAのクエリ言語)が生成され、Hibernateを通じてSQLが実行されます。名前で表しきれない検索は @Query でJPQLやネイティブSQLを直接書けます。

この記事では、命名キーワードと生成されるJPQL・SQLの対応一覧表、クエリメソッド/@Query/ネイティブクエリ/Specification/Querydslの使い分け判断表、そのまま貼れるEntity・Repository・Serviceの一式を、Spring Data JPA 4.1.1(Spring Boot 4.1.1、jakarta.persistence)の公式リファレンスに沿ってまとめます。各キーワードが最終的に投げるのと同等のSQLは PostgreSQL 16.13 で実際に実行し、結果を載せています。 なお、この環境ではSpringのjarを取得できないため、Javaコード(Spring部分)は未実行です。公式ドキュメントの記述に忠実に書いています。

Spring Data JPA クエリメソッドの早見表

まずは「どう書けば何が生成されるか」の一覧です。JPQL列は公式リファレンス(Spring Data JPA 4.1.1)の表に基づきます。例のエンティティは Member(会員)です。

やりたいこと メソッド名の例 生成されるJPQL(要点)
等しい findByEmail(String) where x.email = ?1(引数が null なら is null)
かつ findByStatusAndAge(String, Integer) where x.status = ?1 and x.age = ?2
または findByStatusOrAge(...) where x.status = ?1 or x.age = ?2
範囲 findByJoinedOnBetween(LocalDate, LocalDate) where x.joinedOn between ?1 and ?2
未満/以下 findByAgeLessThan / findByAgeLessThanEqual x.age < ?1 / x.age <= ?1
より大/以上 findByAgeGreaterThan / findByAgeGreaterThanEqual x.age > ?1 / x.age >= ?1
日付の前後 findByJoinedOnAfter / findByJoinedOnBefore x.joinedOn > ?1 / x.joinedOn < ?1
NULLか findByAgeIsNull / findByAgeIsNotNull x.age is null / x.age is not null
LIKE(%は自分で付ける) findByNameLike("佐藤%") x.name like ?1
前方・後方・部分一致 findByNameStartingWith / EndingWith / Containing like ?1(%を自動付与し、引数の%と_はエスケープ)
否定 findByStatusNot / NotLike / NotIn <> ?1 / not like / not in
いずれかに一致 findByStatusIn(Collection<String>) where x.status in ?1
真偽値 findByActiveTrue() / findByActiveFalse() x.active = true / = false
大文字小文字を無視 findByNameIgnoreCase / ...AllIgnoreCase UPPER(x.name) = UPPER(?1)
並び順を固定 findByStatusOrderByJoinedOnDesc ... order by x.joinedOn desc
先頭N件 findTop3By... / findFirstBy... 結果を先頭N件(Firstは1件)に制限
重複除外 findDistinctByStatus select distinct ...
件数 countByStatus(String) 件数を返す(long)
存在確認 existsByEmail(String) boolean を返す
削除 deleteByStatus(String) / removeBy... 対象を削除(void または削除件数)

先頭の動詞(主語キーワード)は find/read/get/query/search/stream…By、count…By、exists…By、delete/remove…By の4系統で、Top<数>・First<数>・Distinct は動詞と By の間に置きます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Data JPA使い方|クエリメソッド命名一覧と@Query実例」

まず全体像:Spring Data JPAが動く仕組み

flowchart LR
    S[Service] -->|呼ぶ| R[MemberRepository<br>インターフェース]
    R -->|起動時に実装を自動生成| P[Spring Data のプロキシ]
    P -->|メソッド名・@Query を解析| Q[JPQL]
    Q -->|JPAプロバイダ Hibernate| SQL[SQL]
    SQL --> DB[(PostgreSQL)]

ポイントは、メソッド名の解析とJPQLの検証がアプリ起動時に行われることです。プロパティ名の打ち間違いはリクエスト時ではなく起動時のエラーとして見つかるため、早く気づけます。

用語 意味
JPA(Jakarta Persistence) Javaのオブジェクトと表を対応させる標準仕様。Spring Boot 4.1 は jakarta.persistence 3.2 系
Hibernate JPAの実装。Spring Bootの既定
Entity 表に対応するクラス(@Entity)
Repository 検索・保存の窓口となるインターフェース
JPQL エンティティ名・フィールド名で書くクエリ言語。表名・列名ではない

コピペで動く一式(Spring Boot 4.1 / Java 21)

依存関係(Maven)

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.1.1</version>
</parent>

<properties>
  <java.version>21</java.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
  </dependency>
</dependencies>

application.yml(SQLログつき)

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/sample
    username: app
    password: app
  jpa:
    hibernate:
      ddl-auto: validate      # 本番はスキーマ管理ツールで作り、ここは validate か none
    open-in-view: false       # 画面描画中の遅延ロードを防ぎ、取得範囲をServiceで明示する
logging:
  level:
    org.hibernate.SQL: debug                 # 実行SQL
    org.hibernate.orm.jdbc.bind: trace       # バインド値(Hibernate 6以降のロガー名)

SQLを確認するときは spring.jpa.show-sql より org.hibernate.SQL のロガーがおすすめです。ログ設定にまとめられ、バインド値も出せます。

テーブル(PostgreSQL)

CREATE TABLE member (
  id bigserial PRIMARY KEY,
  name varchar(50) NOT NULL,
  email varchar(100) NOT NULL UNIQUE,
  status varchar(20) NOT NULL,
  age int,
  joined_on date NOT NULL
);
CREATE TABLE orders (
  id bigserial PRIMARY KEY,
  member_id bigint NOT NULL REFERENCES member(id),
  item varchar(50) NOT NULL,
  amount int NOT NULL
);

Entity

package com.example.member;

import jakarta.persistence.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "member")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

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

    @Column(nullable = false, length = 20)
    private String status;

    private Integer age;

    @Column(name = "joined_on", nullable = false)
    private LocalDate joinedOn;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

    protected Member() { }   // JPAが使う引数なしコンストラクタ

    public Member(String name, String email, String status, Integer age, LocalDate joinedOn) {
        this.name = name; this.email = email; this.status = status;
        this.age = age; this.joinedOn = joinedOn;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
    public String getStatus() { return status; }
    public Integer getAge() { return age; }
    public LocalDate getJoinedOn() { return joinedOn; }
    public List<Order> getOrders() { return orders; }
    public void changeStatus(String status) { this.status = status; }
}
package com.example.member;

import jakarta.persistence.*;

@Entity
@Table(name = "orders")   // ORDER は予約語なので表名を orders にしている
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "member_id")
    private Member member;

    private String item;
    private int amount;

    protected Order() { }
    public Long getId() { return id; }
    public Member getMember() { return member; }
    public String getItem() { return item; }
    public int getAmount() { return amount; }
}

javax.persistence ではなく jakarta.persistence をimportします。 Spring Boot 3以降はJakarta EE 9+の名前空間です。2022年ごろの記事のコードを使うときは、importをここだけ置き換えれば多くはそのまま動きます。

Repository

package com.example.member;

import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;

public interface MemberRepository extends JpaRepository<Member, Long>,
                                          JpaSpecificationExecutor<Member> {

    // ---- クエリメソッド ----
    Optional<Member> findByEmail(String email);
    List<Member> findByStatusAndAgeGreaterThanEqual(String status, int age);
    List<Member> findByJoinedOnBetween(LocalDate from, LocalDate to);
    List<Member> findByAgeIsNull();
    List<Member> findByNameContaining(String keyword);
    List<Member> findByStatusIn(Collection<String> statuses);
    List<Member> findTop2ByStatusOrderByJoinedOnDesc(String status);
    long countByStatus(String status);
    boolean existsByEmail(String email);
    Page<Member> findByStatus(String status, Pageable pageable);
    List<Member> findByStatus(String status, Sort sort);

    // ---- @Query(JPQL) ----
    @Query("""
           select new com.example.member.MemberTotal(m.name, count(o), coalesce(sum(o.amount), 0))
           from Member m left join m.orders o
           group by m.id, m.name
           order by coalesce(sum(o.amount), 0) desc
           """)
    List<MemberTotal> summarizeOrders();

    // ---- @Query(ネイティブSQL+ページング) ----
    @NativeQuery(value = "SELECT * FROM member WHERE status = :status",
                 countQuery = "SELECT count(*) FROM member WHERE status = :status")
    Page<Member> findNativeByStatus(@Param("status") String status, Pageable pageable);

    // ---- 更新・削除 ----
    @Modifying(clearAutomatically = true, flushAutomatically = true)
    @Query("update Member m set m.status = :status where m.joinedOn < :date")
    int updateStatusJoinedBefore(@Param("status") String status, @Param("date") LocalDate date);

    // ---- Projection ----
    List<MemberSummary> findSummaryByStatus(String status);      // interface
    List<MemberNameEmail> findNameEmailByStatus(String status);  // record
    <T> List<T> findByAgeGreaterThan(int age, Class<T> type);   // 動的

    // ---- N+1 対策 ----
    @EntityGraph(attributePaths = "orders")
    List<Member> findWithOrdersByStatus(String status);
}
package com.example.member;

// JPQLのコンストラクタ式で使うDTO(record)
public record MemberTotal(String name, Long orderCount, Long total) { }
package com.example.member;

public interface MemberSummary {      // インターフェース型Projection(閉じた形)
    String getName();
    String getStatus();
}
package com.example.member;

public record MemberNameEmail(String name, String email) { }   // クラス型Projection

Service

package com.example.member;

import java.time.LocalDate;
import java.util.List;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
public class MemberService {

    private final MemberRepository repository;

    public MemberService(MemberRepository repository) {
        this.repository = repository;
    }

    public Page<Member> activeMembers(int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("id"));
        return repository.findByStatus("ACTIVE", pageable);
    }

    public Member findByEmail(String email) {
        return repository.findByEmail(email)
                .orElseThrow(() -> new IllegalArgumentException("会員が見つかりません: " + email));
    }

    @Transactional
    public int deactivateOldMembers(LocalDate date) {
        return repository.updateStatusJoinedBefore("INACTIVE", date);
    }

    @Transactional
    public Member register(String name, String email, Integer age) {
        if (repository.existsByEmail(email)) {
            throw new IllegalStateException("登録済みのメールアドレスです");
        }
        return repository.save(new Member(name, email, "ACTIVE", age, LocalDate.now()));
    }

    public List<MemberTotal> totals() {
        return repository.summarizeOrders();
    }
}

クラスに @Transactional(readOnly = true)、更新メソッドだけ @Transactional を重ねるのが読み取り中心のServiceの定番構成です(トランザクションが効かないケースは別記事で詳しく扱っています)。

命名キーワードごとの生成SQLと実行結果

ここからは、上のRepositoryメソッドが投げるのと同等のSQLを PostgreSQL 16.13 で実行した結果です(Hibernateが生成する実際のSQLは列の別名などが異なります)。データは5人の会員と6件の注文です。

INSERT INTO member(name,email,status,age,joined_on) VALUES
 ('佐藤 花子','hanako@example.com','ACTIVE',28,'2026-04-01'),
 ('鈴木 一郎','ichiro@example.com','ACTIVE',35,'2026-05-10'),
 ('高橋 健','ken@example.com','INACTIVE',NULL,'2026-06-15'),
 ('田中 100%','tanaka@example.com','ACTIVE',42,'2026-07-20'),
 ('Sato Ken','sato.ken@example.com','ACTIVE',23,'2026-08-05');

And と GreaterThanEqual:findByStatusAndAgeGreaterThanEqual("ACTIVE", 30)

SELECT id,name,age FROM member WHERE status='ACTIVE' AND age>=30 ORDER BY id;
 id |   name    | age 
----+-----------+-----
  2 | 鈴木 一郎 |  35
  4 | 田中 100% |  42
(2 rows)

Between:findByJoinedOnBetween(2026-05-01, 2026-07-31)

 id |   name    | joined_on  
----+-----------+------------
  2 | 鈴木 一郎 | 2026-05-10
  3 | 高橋 健   | 2026-06-15
  4 | 田中 100% | 2026-07-20
(3 rows)

Betweenは両端を含みます(>= と <=)。日時(LocalDateTime)で「その日いっぱい」を指定したいときは、GreaterThanEqual と LessThan(翌日0時)の組み合わせにすると境界がはっきりします。

IsNull:findByAgeIsNull()

 id |  name   
----+---------
  3 | 高橋 健
(1 row)

findByAge(null) と書いても is null に変換されますが、意図が読み取りやすいので IsNull を明示するのがおすすめです。

Containing と Like の違い(エスケープを確認)

Containing は引数の % と _ を自動でエスケープし、文字そのものとして検索します。% という1文字で検索したときの差を比べました。

-- エスケープしない場合(LIKE '%%%' は「何でも一致」)
SELECT count(*) FROM member WHERE name LIKE '%%%';
-- Containing("%") 相当(%をエスケープ)
SELECT count(*) FROM member WHERE name LIKE '%\%%' ESCAPE '\';
 count 
-------
     5
(1 row)

 count 
-------
     1
(1 row)

Containing/StartingWith/EndingWith は利用者の入力をそのまま渡しても意図どおりに動きます。 一方 Like は % を自分で付ける前提です。

-- Like("佐藤")  → 0件(%がないので完全一致になる)
-- Like("佐藤%") → 1件

IgnoreCase:findByNameIgnoreCase("sato ken")

 id |   name   
----+----------
  5 | Sato Ken
(1 row)

UPPER(name) = UPPER(?) になるため、大量データで使う場合は upper(name) の式インデックスを用意すると検索が速くなります。

Top と OrderBy:findTop2ByStatusOrderByJoinedOnDesc("ACTIVE")

 id |   name    | joined_on  
----+-----------+------------
  5 | Sato Ken  | 2026-08-05
  4 | 田中 100% | 2026-07-20
(2 rows)

count と exists

-- countByStatus("ACTIVE")
 count 
-------
     4

-- existsByEmail("ken@example.com"):1行取れれば true
 id 
----
  3

存在確認は findBy...().isPresent() より existsBy... が軽量です。全列を読み込まず、1行あるかだけを確認します。

Pageable:findByStatus("ACTIVE", PageRequest.of(1, 2, Sort.by("id")))

Page を返すと、本体のSQLと件数のSQLの2本が実行されます。ページ番号は0始まりなので、of(1, 2) は「2ページ目、2件ずつ」です。

SELECT id,name FROM member WHERE status='ACTIVE' ORDER BY id OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY;
SELECT count(id) FROM member WHERE status='ACTIVE';
 id |   name    
----+-----------
  4 | 田中 100%
  5 | Sato Ken
(2 rows)

 count 
-------
     4
(1 row)
戻り値 件数SQL 向いている画面
Page<T> 実行する 総件数・総ページ数を表示する一覧
Slice<T> 実行しない(1件多く取って次の有無を判定) 「もっと見る」形式
List<T> + Pageable 実行しない 件数が不要な一覧
Window<T> + ScrollPosition 実行しない 大量データのスクロール(キーセット方式も可)

@Query のDTO集計:summarizeOrders()

   name    | order_count | total 
-----------+-------------+-------
 Sato Ken  |           2 |  1100
 田中 100% |           1 |   450
 佐藤 花子 |           2 |   420
 鈴木 一郎 |           1 |   200
 高橋 健   |           0 |     0
(5 rows)

集計結果はエンティティではなくrecordのDTOで受けると、型が明確で、不要な列も読み込みません。JPQLでは count() と sum() が Long を返すため、recordの型も Long にそろえます。

@Modifying の一括更新:updateStatusJoinedBefore("INACTIVE", 2026-05-01)

BEGIN
UPDATE 1
ROLLBACK

戻り値の int にはこの「更新件数(1)」が入ります。一括更新は永続化コンテキスト(JPAが保持しているエンティティのキャッシュ)を経由しないため、clearAutomatically = true で更新後にキャッシュを消し、flushAutomatically = true で更新前に未反映の変更を書き出しておくと、古い値を読む状態を避けられます。

クエリの書き方の使い分け判断表

方法 向いている場面 書き方 注意点
クエリメソッド 条件が2〜3個までの定型検索 メソッド名 名前が長くなったら他の方法へ
@Query(JPQL) JOIN・集計・DTO取得・条件が多い固定検索 @Query("select ...") エンティティ名とフィールド名で書く
@NativeQuery/nativeQuery = true DB固有機能(ウィンドウ関数、jsonb演算子、CTEなど) 生SQL DBを替えると書き直し。ページングは countQuery を明示
Specification 画面の検索フォームなど、条件の有無が実行時に変わる Criteria API をラムダで組み立て 標準機能だけで書ける。可読性は少し下がる
Querydsl 動的条件が多く、型安全に書きたい 生成されたQクラスで記述 APT(注釈処理)でQクラス生成の設定が必要
EntityManager 直接 上記で表せない特殊な処理 カスタムRepository実装 最後の手段として使う

迷ったら「固定条件ならクエリメソッド→@Query、可変条件ならSpecification」と覚えておくと選びやすくなります。メソッド名が findByStatusAndAgeGreaterThanEqualAndJoinedOnAfterOrderByIdDesc のように長くなったら、@Queryに切り替える合図です。

Specificationで「入力された条件だけ」で検索する

package com.example.member;

import java.util.ArrayList;
import java.util.List;
import org.springframework.data.jpa.domain.Specification;

public final class MemberSpecs {
    private MemberSpecs() { }

    static Specification<Member> statusIs(String status) {
        return (root, query, cb) -> cb.equal(root.get("status"), status);
    }
    static Specification<Member> nameContains(String keyword) {
        return (root, query, cb) -> cb.like(root.get("name"), "%" + keyword + "%");
    }
    static Specification<Member> ageAtLeast(int age) {
        return (root, query, cb) -> cb.greaterThanOrEqualTo(root.get("age"), age);
    }

    /** null・空の条件は無視して AND で結合する */
    public static Specification<Member> search(String status, String keyword, Integer minAge) {
        List<Specification<Member>> specs = new ArrayList<>();
        if (status != null && !status.isBlank()) specs.add(statusIs(status));
        if (keyword != null && !keyword.isBlank()) specs.add(nameContains(keyword));
        if (minAge != null) specs.add(ageAtLeast(minAge));
        Specification<Member> result = (root, query, cb) -> cb.conjunction(); // 常に真
        for (Specification<Member> s : specs) result = result.and(s);
        return result;
    }
}
Page<Member> page = repository.findAll(
        MemberSpecs.search("ACTIVE", null, 30),
        PageRequest.of(0, 20, Sort.by("joinedOn").descending()));

Spring Data JPA 4.0からは、クエリの種類に依存しない PredicateSpecification や、一括更新・削除用の UpdateSpecification/DeleteSpecification も加わっています。like に利用者の入力を渡す場合は、クエリメソッドの Containing と違って自動エスケープされない点に注意します。

Projection:必要な列だけ取得する

種類 定義 特徴
インターフェース型(閉じた形) getterだけのinterface 必要な列だけSELECTされる。ネストも可
インターフェース型(開いた形) @Value("#{target.name + ...}") 計算値を返せるが、列の絞り込みは効かない
クラス型(DTO) record(コンストラクタが1つ) プロキシを使わず軽量。ネストしたProjectionは不可
動的 <T> List<T> findBy...(..., Class<T> type) 呼び出し側で型を選ぶ
List<MemberSummary> a = repository.findSummaryByStatus("ACTIVE");           // interface
List<MemberNameEmail> b = repository.findNameEmailByStatus("ACTIVE");       // record
List<MemberNameEmail> c = repository.findByAgeGreaterThan(30, MemberNameEmail.class);
List<Member> d = repository.findByAgeGreaterThan(30, Member.class);         // エンティティそのもの

読み取り専用の一覧はrecordのProjectionにすると、エンティティの変更検知(ダーティチェック)の対象にならず、メモリ消費も抑えられます。公式リファレンスによると、@Query でエンティティを選んでいてもDTO型を戻り値にすれば、Spring Data JPAがコンストラクタ式へ書き換えます。その場合、JPQLの列に別名(as)を付けない点がポイントです。

N+1を@EntityGraphで1本のSQLにする

Member を一覧取得してから各会員の getOrders() を呼ぶと、会員の数だけ注文のSQLが追加で実行されます(N+1問題。原因と見つけ方は別記事で詳しく扱っています)。Spring Data JPAでは @EntityGraph(attributePaths = "orders") を付けるだけでJOINして同時に取得できます。

-- findWithOrdersByStatus("ACTIVE") 相当
SELECT m.id, m.name, o.item FROM member m LEFT JOIN orders o ON o.member_id=m.id
WHERE m.status='ACTIVE' ORDER BY m.id, o.id;
 id |   name    |    item    
----+-----------+------------
  1 | 佐藤 花子 | ノート
  1 | 佐藤 花子 | ペン
  2 | 鈴木 一郎 | 付箋
  4 | 田中 100% | ファイル
  5 | Sato Ken  | ノート
  5 | Sato Ken  | ホッチキス
(6 rows)

1対多をJOINすると行数が「注文の件数」に増える点に注意します。Hibernate 6以降はエンティティの重複を自動でまとめますが、コレクションのJOIN取得と Pageable を組み合わせると、ページングがメモリ上で行われる旨の警告になります。ページングしたい一覧は「まず会員IDだけをページングで取得 → そのIDで @EntityGraph 取得」の2段階にするか、spring.jpa.properties.hibernate.default_batch_fetch_size で IN 句にまとめる方法が扱いやすいです。

よくあるエラーと対処

エラー文はSpring Data・Hibernateのバージョンにより細部が異なります。代表的な形と対処をまとめます(この記事の環境では未実行のため、文言は公式の例外クラスとメッセージ形式に基づく代表例です)。

エラー(代表的なメッセージ) 原因 対処
PropertyReferenceException: No property 'nam' found for type 'Member' メソッド名のプロパティ名がフィールド名と一致しない フィールド名(列名ではない)に合わせる。joined_on 列なら JoinedOn
QueryCreationException / Could not create query for public abstract ... メソッド名を解析できない、キーワードの綴りや順序の誤り 早見表のキーワードと照合。長いなら@Queryへ
Validation failed for query for method ... @QueryのJPQLに誤り(表名で書いた、別名の誤りなど) JPQLはエンティティ名 Member・フィールド名 joinedOn で書く
IncorrectResultSizeDataAccessException: Query did not return a unique result: 2 results were returned 単一の戻り値(Optional<T>・T)なのに複数件が該当 一意な条件にする、findFirstBy... にする、List で受ける
InvalidDataAccessApiUsageException(update/delete の実行に関するもの) @Query の更新系に @Modifying がない、またはトランザクション外で実行 @Modifying を付け、Serviceの @Transactional メソッドから呼ぶ
LazyInitializationException: ... could not initialize proxy - no Session トランザクション終了後に遅延ロードのコレクションへアクセス @EntityGraph・JOIN FETCH で先に取得する、DTOで返す
Cannot resolve parameter 系(名前付きパラメータが見つからない) :status に対応する引数名を解決できない @Param("status") を付ける(またはコンパイル時に -parameters)
ConverterNotFoundException(Projection変換時) DTOのコンストラクタ引数の型がJPQLの結果と合わない count()・sum() は Long で受ける

つまずきの多くは「JPQLはJavaのクラス名・フィールド名で書く」「メソッド名はフィールド名に合わせる」の2点に集約されます。 起動時エラーのメッセージにはメソッド名が含まれるので、そこから該当箇所を特定できます。

設計で気をつけたいチェックリスト

チェック項目 理由
一覧APIはProjection(record)で返している 不要な列・変更検知を避けられる
open-in-view: false にしている 取得範囲をService層で明示でき、想定外のSQLを防げる
Page が必要かを確認した 件数SQLが不要なら Slice/List で1本減らせる
@Modifying に clearAutomatically を付けた 一括更新後に古いエンティティを読まない
1対多の取得は @EntityGraph かバッチフェッチ N+1を防ぐ
外部入力のLIKEは Containing 系、自前のlikeはエスケープ % _ を文字として扱う
SQLログを開発環境で出している 生成SQLと実行回数を目で確かめられる

よくある質問

Q. Spring Data JPAとJPA(Hibernate)の違いは何ですか? A. JPAはORM(オブジェクトと表の対応)の標準仕様、Hibernateはその実装、Spring Data JPAはその上でRepositoryの実装を自動生成する仕組みです。Spring Data JPAを使っても、実際にSQLを作って実行するのはHibernateです。

Q. クエリメソッドと@Query、どちらを使うべきですか? A. 条件が2〜3個までならクエリメソッド、それ以上やJOIN・集計を含むなら@Queryが読みやすくなります。条件の有無が実行時に変わる検索はSpecificationを使います。

Q. ネイティブクエリでもページングできますか? A. できます。@NativeQuery に countQuery を指定すると確実です。ソートは Sort のプロパティ名がそのままSQLに入るため、列名と一致させる必要があります。

Q. javax.persistence のサンプルはそのまま使えますか? A. Spring Boot 3以降(4.1を含む)は jakarta.persistence です。importを jakarta.persistence.* に置き換えるのが最初の手順です。

まとめ

  • クエリメソッドは「動詞+By+条件キーワード」で、JPQLが自動生成される
  • Containing/StartingWith/EndingWithは % _ を自動エスケープ、Likeは自分で % を付ける
  • 固定条件はクエリメソッド→@Query、可変条件はSpecificationで使い分ける
  • Page は本体と件数の2本のSQL。件数が不要なら Slice や List
  • @Modifyingは clearAutomatically とトランザクションをセットで
  • 一覧はrecordのProjection、関連は@EntityGraphで取得範囲を明示する
  • エラーの多くはフィールド名とJPQLの書き方の確認で解決できる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Data JPA使い方|クエリメソッド命名一覧と@Query実例」

株式会社RJCでは、Spring Data JPAのようなデータアクセスの基本を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?