はじめに
Spring Boot + JPA/Hibernate で開発していると、「ほとんど変わらない」同じデータ群を何度も繰り返しクエリすることがよくあります。たとえば国リスト、商品カテゴリ、システム設定などです。ここで自然に浮かぶのが、「毎回データベースを叩かず、キャッシュしておけないか?」という発想です。
答えは、できます。実は Hibernate には二層のキャッシュ機構が組み込まれています。一次キャッシュと二次キャッシュです。一次キャッシュはデフォルトで有効になっていて、普段その存在を意識することはほとんどありません。一方で二次キャッシュは追加の設定が必要で、しかも最近は語られることが比較的少なくなっています。この記事では次のことを理解していきます。
- 二次キャッシュとは何か?一次キャッシュとの違いは?
- 二次キャッシュを実際にどう使うのか(完全なサンプル付き)
- どんなときに使うべきで、どんなときに使うべきでないか?そしてなぜ今はあまり耳にしないのか?
二次キャッシュ
二次キャッシュとは?一次キャッシュとの違いは?
二次キャッシュを理解するには、まず一次キャッシュから話す必要があります。
一次キャッシュ(First-Level Cache)
一次キャッシュのスコープは Hibernate Session(JPA でいう EntityManager の Persistence Context)です。ライフサイクルは通常「一つのトランザクション」と同じです。
その挙動はこうです。同じ Session の中では、同じ id で同じ entity をクエリしても、2 回目以降はデータベースを叩かず、Session 内のキャッシュから直接返します。
@Transactional
public void demo() {
// 1 回目のクエリ:SELECT を発行し、データベースを叩く
User u1 = entityManager.find(User.class, 1L);
// 2 回目のクエリ:同じ Session・同じ id なので SQL は発行されず、キャッシュから返る
User u2 = entityManager.find(User.class, 1L);
System.out.println(u1 == u2); // true — まったく同じインスタンス
}
一次キャッシュには 2 つの重要な特徴があります。
- デフォルトで有効、かつ無効化できません。 これは Hibernate が「同一トランザクション内のオブジェクト一貫性」を保証するための土台です。
- スコープがとても小さいです。 Session が終われば(トランザクションが終われば)、キャッシュもそれとともに消えます。別の Session、別のリクエストになれば、すべてリセットされ、また改めてデータベースをクエリすることになります。
補足:一次キャッシュを回避して強制的に再クエリするには?
ときには「キャッシュを使わず、直接データベースを叩き直したい」という要求があります(たとえば他の誰かにデータを変更された疑いがあるとき)。以下は 4 つの一般的な方法です。
方法 1:entityManager.refresh(entity) — 単一の entity について SELECT を発行し直し、データベースの最新値で上書きします。
User user = entityManager.find(User.class, 1L);
// 他の誰かがデータを変更したかもしれない
entityManager.refresh(user); // SELECT を発行し直し、現在の状態を上書き
方法 2:entityManager.clear() — Persistence Context 全体を空にするので、以降のクエリはすべてデータベースを叩き直します。
User u1 = entityManager.find(User.class, 1L);
entityManager.clear(); // Persistence Context 全体をクリア
User u2 = entityManager.find(User.class, 1L); // SELECT を発行し直す
方法 3:entityManager.detach(entity) — Persistence Context から単一の entity だけを取り除きます。clear() よりスコープが正確です。
User u1 = entityManager.find(User.class, 1L);
entityManager.detach(u1); // この 1 件だけを取り除く
User u2 = entityManager.find(User.class, 1L); // SELECT を発行し直す
方法 4:新しい Session を開く — 一次キャッシュは Session とともに生まれて消えます。新しい Session(新しいトランザクション)に切り替えれば、自然と真新しい空のキャッシュになり、すべてデータベースから改めてクエリされます。
二次キャッシュ(Second-Level Cache)
二次キャッシュのスコープは SessionFactory(アプリケーション全体で共有)です。ライフサイクルは複数の Session、複数のトランザクション、さらには複数のリクエストにまたがります。
つまり、ユーザー A のリクエストがある User を二次キャッシュに読み込んだあと、ユーザー B のリクエスト(別の Session)が同じレコードをクエリしても、データベースを叩かずにそのままキャッシュにヒットできるのです。これはまさに一次キャッシュにはできないことです。
ただし、タダではありません。
- デフォルトでは無効です。 キャッシュプロバイダー(EhCache、Caffeine、Infinispan、Hazelcast…)を追加で導入し、手動で設定して初めて有効になります。
- entity に
@Cacheableを付け、concurrency 戦略を指定して、「この entity だけをキャッシュする」と Hibernate に明示的に伝える必要があります。
両者の違いのまとめ
| 比較項目 | 一次キャッシュ | 二次キャッシュ |
|---|---|---|
| スコープ | Session(単一トランザクション) | SessionFactory(アプリ全体で共有) |
| ライフサイクル | トランザクション終了で消える | トランザクション・リクエストをまたぐ |
| デフォルトで有効か | はい、かつ無効化不可 | いいえ、追加設定が必要 |
| Session 間で共有か | いいえ | はい |
| プロバイダーが必要か | 不要(組み込み) | 必要(EhCache、Caffeine…) |
| 典型的な用途 | 単一トランザクション内の一貫性 | 「読み多く書き少ない」共有データの保持 |
一言でいえば、一次キャッシュは「同じトランザクション内で二度クエリしない」ことを、二次キャッシュは「異なるトランザクション間でも二度クエリしない」ことを担っています。
二次キャッシュの使い方(サンプル付き)
以下では EhCache(JCache 標準経由で連携)を使って、一連の流れを示します。プロジェクトにはすでに Spring Boot + JPA があるものとします。
ステップ 1:依存関係を追加する
Hibernate は公式に、JCache(JSR-107)標準を通じてキャッシュプロバイダーと連携することを推奨しています。こうしておくと、後でプロバイダーを差し替えやすくなります。Maven の例です。
<!-- Hibernate の JCache ブリッジ -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jcache</artifactId>
</dependency>
<!-- 実際のキャッシュプロバイダー:EhCache 3 -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<classifier>jakarta</classifier>
</dependency>
ステップ 2:二次キャッシュの設定を有効にする
application.properties で有効化します。
# 二次キャッシュを有効化
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
# region factory として JCache を使用
spring.jpa.properties.hibernate.cache.region.factory_class=jcache
# (任意)クエリキャッシュを有効化。クエリ結果の id リストをキャッシュする
spring.jpa.properties.hibernate.cache.use_query_cache=true
# (推奨)キャッシュのヒット状況を観察するための統計情報
spring.jpa.properties.hibernate.generate_statistics=true
ステップ 3:Entity に @Cacheable を付ける
付与された entity だけが二次キャッシュに入ります。ここでのポイントは @Cache の concurrency 戦略です。
import jakarta.persistence.Cacheable;
import jakarta.persistence.Entity;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Cacheable // この entity がキャッシュ可能であることを示す JPA 標準アノテーション
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // Hibernate で concurrency 戦略を指定
public class Product {
@Id
private Long id;
private String name;
private BigDecimal price;
// getters / setters ...
}
concurrency 戦略の選択はきわめて重要です。よく使う 4 つを挙げます。
| 戦略 | 適した場面 |
|---|---|
READ_ONLY | 読み取り専用(例:国コード)。パフォーマンスが最も良い |
NONSTRICT_READ_WRITE | まれに更新され、短時間の不整合を許容できる |
READ_WRITE | 更新が必要。ソフトロックで一貫性を保つ。最もよく使われる |
TRANSACTIONAL | JTA トランザクション管理が必要。完全なトランザクション隔離の場面 |
ステップ 4:ヒットしているか検証する
Session をまたいでクエリするコードを書き、SQL ログを観察します。
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
@Transactional(readOnly = true)
public Product getProduct(Long id) {
return productRepository.findById(id).orElseThrow();
}
}
2 つの異なるリクエスト(異なるトランザクション)から getProduct(1L) を呼び出します。
- 1 回目:コンソールに
select ... from product where id = ?が出力され、データが二次キャッシュに入ります。 - 2 回目:SQL は出力されず、データは二次キャッシュから直接取得されます。
generate_statistics を有効にしていれば、SessionFactory の Statistics から SecondLevelCacheHitCount(ヒット回数)を観察することで、キャッシュが本当に効いているか確認できます。
補足:二次キャッシュが保持するのは entity の「分解された状態」(id と各フィールド値の対応)であって、Java オブジェクト丸ごとではありません。ヒットのたびに Hibernate は新しい entity インスタンスへ再構築するので、Session をまたいで取得したものは同一のオブジェクトではありません。ここが一次キャッシュとの違いです。
どんなときに使うべきか?使うべきでないか?なぜあまり耳にしないのか?
向いている場面
二次キャッシュが理想的なのは、「読み多く書き少なく、かつ共有してよい」データです。
- 参照データ(reference data):国、通貨、商品カテゴリ、権限リストといった、ほとんど変わらないデータ。
- 高頻度の読み取り・低頻度の更新:読み取りが書き込みをはるかに上回り、ヒット率が高くて効果がはっきり出る。
- 単一のシステムがデータベースを専有:このサービスだけがこのテーブルに書き込むので、キャッシュがずれにくい。
向いていない場面
- 頻繁に更新されるデータ:書き込みのたびにキャッシュを無効化(invalidate)する必要があり、節約できるクエリより維持コストが高くつくことがあり、むしろ遅くなることさえあります。
- 複数のサービス/複数のノードが同じデータベースに書き込む:他のサービスがデータベースを直接変更しても、Hibernate の二次キャッシュはそれを知らないため、古い汚れたデータを掴んでしまいます。これは二次キャッシュの最も悪名高い落とし穴です。
- クラスタ構成(複数マシン):各ノードがローカルキャッシュを個別に持ち、互いに同期しません。そのため分散キャッシュ(Infinispan、Hazelcast)を導入して同期する必要が出てきて、複雑さが一気に増します。
なぜ今はあまり耳にしないのか?
役に立たないわけではなく、時代とアーキテクチャが変わり、その役割が別のソリューションに取って代わられたのです。
-
マイクロサービス + 分散アーキテクチャが主流になった。 二次キャッシュは「データベースは自分が専有し、すべての書き込みを掌握している」ことを前提とします。しかしマイクロサービスの世界では、データは複数のサービスで共有されたりイベント経由で変更されたりします。あるインスタンスが、他のインスタンス上の Hibernate キャッシュを自動的に evict することはなく、一貫性の問題に人々は二の足を踏みます。
-
みんな「アプリケーション層のキャッシュ」に移行し、明示的な制御を好むようになった。 今より一般的なのは、Spring の
@Cacheableキャッシュ抽象を Redis と組み合わせる方法です。ORM から疎結合で、サービス間で共有でき、無効化戦略(TTL、能動的な evict)を自分で握れます。意図も明確で——「どのロジックの結果がキャッシュされたか」がはっきり分かり、ORM の奥底に埋もれません。外部サービスに依存することにはなりますが、こうしたやり方が主流になっています。
// 今より一般的なやり方:Spring Cache 抽象 + Redis。明示的で制御しやすい
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productRepository.findById(id).orElseThrow();
}
-
暗黙的なキャッシュはデバッグしづらい。 二次キャッシュは Hibernate の内部に隠れているため、汚れたデータやパフォーマンスの問題が出たとき、キャッシュが犯人なのかを追跡するのが難しいことが多いです。それに比べ、明示的なキャッシュ層はより透明で運用しやすいのです。
-
「まず計測、それから最適化」という考え方が広まった。 パフォーマンスの問題の多くは、実は N+1 クエリやインデックス不足に由来します。これらは
JOIN FETCHやインデックス追加で解決でき、二次キャッシュのような重い機構を持ち出す必要はありません。
まとめ
この記事では、Hibernate の二次キャッシュを一から整理しました。
- ✅ 一次キャッシュは単一の Session/トランザクションにスコープされ、デフォルトで有効・無効化不可、トランザクション内のオブジェクト一貫性を保証する。
- ✅ 二次キャッシュは SessionFactory 全体にスコープされ、トランザクション・リクエストをまたいで共有されるが、追加のプロバイダーと設定が必要。
- ✅ 使い方は 4 ステップ:依存を追加 → 設定を有効化 →
@Cacheableを付け concurrency 戦略を選ぶ → ヒットを検証。 - ✅ 「読み多く書き少なく、単一サービスが専有する」参照データに向く。頻繁な更新、複数サービスの書き込み、クラスタ構成に出くわしたら、汚れたデータに注意。
- ✅ あまり語られなくなったのは、マイクロサービスと分散アーキテクチャによってその前提が成り立ちにくくなり、人々がより明示的で制御しやすい Redis + Spring Cache 抽象 へ移ったからです。
二次キャッシュの本質はこうです。それは「あなたのデータへのすべての変更が Hibernate を経由する」ことを前提とするキャッシュなのです。 その前提が成り立つなら、ほぼゼロ侵襲で膨大なクエリを節約できます。前提が成り立たないなら、もたらされる汚れたデータのリスクがメリットを上回ります。この前提を理解すれば、なぜかつて流行し、なぜ徐々に主流から外れていったのかが分かるはずです。
もちろん、分散アーキテクチャのもとでも、すべてのデータが Redis に入れるのに適しているわけではありません。キャッシュツールはあくまで手段にすぎず、本当に設計すべきなのはデータ一貫性モデル、キャッシュ無効化戦略(Cache Invalidation)、データのライフサイクル、そして更新フローです。システムがマイクロサービスを採用しているというだけで、一律に Hibernate の二次キャッシュを Redis で置き換えるべきではありません。
二次キャッシュが適しているかどうかは、最終的にはビジネスがデータ一貫性にどれだけ厳しいかによって決まります。 システムが短時間の古いデータを許容でき、かつ適切な無効化戦略(たとえば TTL やイベント通知)を備えているなら、二次キャッシュがもたらす効果はリスクを上回るかもしれません。 逆に、読み取りのたびに必ず最新のデータを得なければならないなら、慎重に評価すべきであり、使用を避けることさえ検討すべきです。