- Spring
Spring Data JPA使い方|クエリメソッド命名一覧と@Query実例
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期連続の黒字決算。