Dev Study
← 解説「JPA の永続化コンテキスト」に戻る

サンプルコードで身につける: JPA の永続化コンテキスト

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1エンティティの定義と、最初の保存で出る SQL

この回の実験で使う2つので、Author が Book を複数持ち、どちらの向きの関連も後から読みに行く設定()です。Author に Book を2つ足して save を1回呼ぶと、ID の採番と3件の insert が出ました。Book の insert まで出るのは books に cascade = CascadeType.ALL を付けているためで、SQL の文面は 7.4.5 と でのものです。

service.create() で出た SQL(--spring.jpa.show-sql=true)コマンド出力
Hibernate ORM version = 7.4.5.Final
--- 0. create
Hibernate: select next value for author_seq
Hibernate: select next value for book_seq
Hibernate: select next value for book_seq
Hibernate: insert into author (name,id) values (?,?)
Hibernate: insert into book (author_id,title,id) values (?,?,?)
Hibernate: insert into book (author_id,title,id) values (?,?,?)
@Entity
class Author {
    @Id @GeneratedValue Long id;
    String name;
    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL)   // 既定で LAZY
    List<Book> books = new ArrayList<>();
    protected Author() {}
    Author(String name) { this.name = name; }
}

@Entity
class Book {
    @Id @GeneratedValue Long id;
    String title;
    @ManyToOne(fetch = FetchType.LAZY) Author author;
    protected Book() {}
    Book(String title, Author author) { this.title = title; this.author = author; }
}

interface AuthorRepository extends JpaRepository<Author, Long> {}

// AuthorService の中
// @Transactional
// public Long create() {
//     Author a = new Author("old name");
//     a.books.add(new Book("Book A", a));
//     a.books.add(new Book("Book B", a));
//     return repo.save(a).id;
// }

2同じ ID を2回読む: 1つのトランザクションで囲む場合と囲まない場合

同じ ID で findById を2回呼ぶと、 のの中では SELECT は1回だけで、a1 == a2 は true です。自分で囲まずに呼んだ3番では SELECT が2回出て、b1 == b2 は false でした。 4.1.1 の にはに @Transactional(readOnly = true) が付いており、DEBUG ログでは findById の呼び出しごとにが始まり、そのたびに別の が開かれて閉じられていました。

実行結果(Hibernate ORM 7.4.5.Final / H2)コマンド出力
--- 2. findById twice inside one transaction
Hibernate: select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
a1 == a2 : true
--- 3. findById twice with no surrounding transaction
Hibernate: select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
Hibernate: select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
b1 == b2 : false
// AuthorService の中(1つのトランザクション)
@Transactional
public void findTwiceInOneTransaction(Long id) {
    Author a1 = repo.findById(id).orElseThrow();
    Author a2 = repo.findById(id).orElseThrow();
    System.out.println("    a1 == a2 : " + (a1 == a2));
}

// main の中(自分では囲んでいない。findById が呼び出しごとに自分のトランザクションを始める)
// Author b1 = repo.findById(id).orElseThrow();
// Author b2 = repo.findById(id).orElseThrow();
// System.out.println("    b1 == b2 : " + (b1 == b2));

3遅延ロード: 机があるうちは SQL が出て、片付いた後は例外になる

Author の books は、触ったときに初めて読みに行くコレクションです(実クラスは PersistentBag)。@Transactional のメソッドの中で size() を呼ぶと、その時点で book への SELECT が出ました。5番の Author は囲まずに呼んだ findById が返したもので、読んだときの机はもう閉じられているため になり、メッセージ末尾の (no session) がそれを表しています。

実行結果(4番は @Transactional の中、5番は main から直接 b1.books.size()。見出しの行は実験プログラムの出力のまま)追跡ログ
--- 4. lazy collection inside a transaction
Hibernate: select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
books collection class = org.hibernate.collection.spi.PersistentBag
Hibernate: select b1_0.author_id,b1_0.id,b1_0.title from book b1_0 where b1_0.author_id=?
size = 2
--- 5. lazy collection outside a transaction
org.hibernate.LazyInitializationException
Cannot lazily initialize collection of role 'l22.jpa.Author.books' with key '1' (no session)

4Web アプリでは open-in-view の既定が効く

@Transactional の無いコントローラで a.books.size() を呼ぶ URL を叩いた結果です(--- で始まる行は区切りとして足したもの)。 の既定は true で、起動時に WARN が1行出て、リクエストはにならずにその場で SQL が出て 200 が返りました。false にして起動すると同じリクエストが 500 になり、WARN は出なくなります。

GET /authors/first/book-count(Spring Boot 4.1.1。ログは行頭の時刻を落とした抜粋)コマンド出力
--- 既定のまま起動(起動時の WARN と、リクエスト時の SQL)
WARN 1 --- [data] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
Hibernate: select a1_0.id,a1_0.name from author a1_0
Hibernate: select b1_0.author_id,b1_0.id,b1_0.title from book b1_0 where b1_0.author_id=?
 
--- --spring.jpa.open-in-view=false で起動(応答とログ)
HTTP/1.1 500
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 01 Oct 2026 13:34:29 GMT
Connection: close
 
{"timestamp":"2026-10-01T13:34:29.675Z","status":500,"error":"Internal Server Error","path":"/authors/first/book-count"}
 
ERROR 1 --- [data] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.hibernate.LazyInitializationException: Cannot lazily initialize collection of role 'l22.jpa.Author.books' with key '1' ...

5素の Java で: 作業机の模型

の考え方を Map でなぞった模型で、SQL: の行は模型が表示している文字列です(DB には接続していません)。ID から実物への対応表があるので2回目の find は DB へ行かず、読んだ時点の写しと比べることで save 無しの変更を見つけます。写しと比べる考え方は、拡張を使わない場合について Hibernate の入門ガイドが説明しているものと同じですが、Map の作りやコミット時にだけ比べる点は説明用で、Hibernate はクエリの前にも書き戻すことがあります。

Java

図やコードに出てくる用語(タップで説明)