Spring JPAアプリケーションにおいて、データベースアクセスの最適化はパフォーマンス向上の鍵となります。Hibernate 2次キャッシュ(Second Level Cache)は、SessionFactory(EntityManagerFactory)レベルでエンティティをキャッシュし、複数のセッション間でデータを共有することで、データベースへのアクセス回数を大幅に削減できる強力な仕組みです。本記事では、Spring Boot環境でのHibernate 2次キャッシュの設定方法、キャッシュプロバイダーの選定基準、CacheConcurrencyStrategyの選び方、クエリキャッシュの活用法、そしてキャッシュ無効化のベストプラクティスを体系的に解説します。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Java 17以上
Spring Boot 3.4.x
Spring Data JPA 3.4.x(Spring Boot Starterに含まれる)
Hibernate 6.6.x(Spring Data JPAに含まれる)
キャッシュプロバイダー Ehcache 3.x / Caffeine / Redis
ビルドツール Maven または Gradle
IDE VS Code または IntelliJ IDEA

事前に以下の準備を完了してください。

  • JDK 17以上のインストール
  • Spring Boot + Spring Data JPAプロジェクトの基本構成
  • Maven または Gradle によるプロジェクトビルド環境

1次キャッシュと2次キャッシュの違い

Hibernateには2種類のキャッシュ機構があります。それぞれの特性を理解することで、適切なキャッシュ戦略を選択できます。

flowchart TB
    subgraph App["アプリケーション"]
        Session1["Session A<br/>(トランザクション1)"]
        Session2["Session B<br/>(トランザクション2)"]
        Session3["Session C<br/>(トランザクション3)"]
    end
    
    subgraph L1["1次キャッシュ"]
        L1_1["Session A の<br/>永続化コンテキスト"]
        L1_2["Session B の<br/>永続化コンテキスト"]
        L1_3["Session C の<br/>永続化コンテキスト"]
    end
    
    subgraph L2["2次キャッシュ(SessionFactory共有)"]
        EntityCache["エンティティキャッシュ"]
        CollectionCache["コレクションキャッシュ"]
        QueryCache["クエリキャッシュ"]
    end
    
    Database[(データベース)]
    
    Session1 --> L1_1
    Session2 --> L1_2
    Session3 --> L1_3
    
    L1_1 --> L2
    L1_2 --> L2
    L1_3 --> L2
    
    L2 --> Database

1次キャッシュの特徴

1次キャッシュは永続化コンテキスト(Persistence Context)そのものであり、以下の特徴を持ちます。

特性 説明
スコープ Session(EntityManager)単位
ライフサイクル トランザクション開始から終了まで
共有範囲 同一セッション内のみ
有効化 デフォルトで有効(無効化不可)
設定 不要

1次キャッシュは、同一トランザクション内で同じエンティティを複数回取得する際にデータベースアクセスを省略します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Service
@Transactional
public class ProductService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void demonstrateFirstLevelCache() {
        // 1回目: データベースからフェッチ
        Product product1 = entityManager.find(Product.class, 1L);
        
        // 2回目: 1次キャッシュから取得(SQLは発行されない)
        Product product2 = entityManager.find(Product.class, 1L);
        
        // 同一オブジェクト参照
        System.out.println(product1 == product2); // true
    }
}

2次キャッシュの特徴

2次キャッシュはSessionFactory(EntityManagerFactory)レベルで動作し、以下の特徴を持ちます。

特性 説明
スコープ SessionFactory(EntityManagerFactory)単位
ライフサイクル アプリケーションの起動から終了まで
共有範囲 すべてのSession(トランザクション)間で共有
有効化 明示的な設定が必要
設定 キャッシュプロバイダーとエンティティ単位の設定が必要

2次キャッシュを使用することで、異なるトランザクション間でもキャッシュされたエンティティを再利用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Service
public class ProductService {
    
    @Autowired
    private ProductRepository productRepository;
    
    // トランザクション1: データベースから取得し、2次キャッシュに格納
    @Transactional
    public Product getProductTransaction1(Long id) {
        return productRepository.findById(id).orElseThrow();
    }
    
    // トランザクション2: 2次キャッシュから取得(SQLは発行されない)
    @Transactional
    public Product getProductTransaction2(Long id) {
        return productRepository.findById(id).orElseThrow();
    }
}

1次キャッシュと2次キャッシュの比較

項目 1次キャッシュ 2次キャッシュ
保存形式 Javaオブジェクト 脱水化(dehydrated)された状態
メモリ効率 低い(オブジェクト全体を保持) 高い(正規化された属性値を保持)
クラスタ対応 不可 プロバイダーにより可能
設定の複雑さ なし 中〜高
一貫性保証 トランザクション内で完全 同時実行戦略に依存

2次キャッシュの有効化

Spring Boot環境で2次キャッシュを有効化するには、依存関係の追加、設定ファイルの編集、エンティティへのアノテーション付与が必要です。

依存関係の追加

JCache(JSR-107)を使用したEhcache 3の設定例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot Starter Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- Hibernate JCache integration -->
    <dependency>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-jcache</artifactId>
    </dependency>
    
    <!-- Ehcache 3 as JCache provider -->
    <dependency>
        <groupId>org.ehcache</groupId>
        <artifactId>ehcache</artifactId>
        <classifier>jakarta</classifier>
    </dependency>
</dependencies>

Gradleを使用する場合は以下のように記述します。

1
2
3
4
5
6
// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.hibernate.orm:hibernate-jcache'
    implementation 'org.ehcache:ehcache:3.10.8:jakarta'
}

application.ymlの設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
spring:
  jpa:
    properties:
      hibernate:
        # 2次キャッシュの有効化
        cache:
          use_second_level_cache: true
          use_query_cache: true
          region:
            factory_class: jcache
        # JCacheプロバイダーの設定
        javax:
          cache:
            provider: org.ehcache.jsr107.EhcacheCachingProvider
            uri: classpath:ehcache.xml
      # 共有キャッシュモード(明示的にキャッシュ可能なエンティティのみ)
      jakarta:
        persistence:
          sharedCache:
            mode: ENABLE_SELECTIVE
    # 開発時のログ出力設定
    show-sql: true
    
logging:
  level:
    org.hibernate.cache: DEBUG

Ehcache設定ファイル

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- src/main/resources/ehcache.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.ehcache.org/v3"
        xsi:schemaLocation="http://www.ehcache.org/v3
            http://www.ehcache.org/schema/ehcache-core-3.0.xsd">
    
    <!-- デフォルトのキャッシュテンプレート -->
    <cache-template name="default">
        <expiry>
            <ttl unit="minutes">30</ttl>
        </expiry>
        <heap unit="entries">1000</heap>
    </cache-template>
    
    <!-- エンティティキャッシュ -->
    <cache alias="com.example.domain.Product" uses-template="default">
        <heap unit="entries">500</heap>
    </cache>
    
    <!-- クエリキャッシュ -->
    <cache alias="default-query-results-region" uses-template="default">
        <heap unit="entries">200</heap>
    </cache>
    
    <!-- タイムスタンプキャッシュ(重要:有効期限を設定しない) -->
    <cache alias="default-update-timestamps-region">
        <heap unit="entries">5000</heap>
    </cache>
</config>

@Cacheableと@Cacheアノテーションの使い方

2次キャッシュを使用するエンティティには、JPA標準の@CacheableアノテーションとHibernate固有の@Cacheアノテーションを組み合わせて設定します。

基本的なエンティティキャッシュ設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import jakarta.persistence.*;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Entity
@Table(name = "products")
@Cacheable  // JPA標準:このエンティティがキャッシュ可能であることを宣言
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)  // Hibernate固有:同時実行戦略を指定
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String name;
    
    @Column(nullable = false)
    private Integer price;
    
    @Column(name = "stock_quantity")
    private Integer stockQuantity;
    
    @Version
    private Integer version;
    
    // コンストラクタ、ゲッター、セッターは省略
}

コレクションキャッシュの設定

エンティティに関連するコレクションもキャッシュできます。ただし、コレクションキャッシュには関連エンティティのIDのみが格納されるため、関連エンティティ自体もキャッシュ対象にする必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Entity
@Table(name = "categories")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Category {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String name;
    
    @OneToMany(mappedBy = "category", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)  // コレクションにも@Cacheを設定
    private List<Product> products = new ArrayList<>();
    
    // コンストラクタ、ゲッター、セッターは省略
}

キャッシュリージョンのカスタマイズ

デフォルトでは、エンティティの完全修飾クラス名がキャッシュリージョン名として使用されますが、region属性でカスタマイズできます。

1
2
3
4
5
6
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "product-cache")
public class Product {
    // フィールド定義
}

キャッシュプロバイダーの選定ポイント

Hibernateは複数のキャッシュプロバイダーをサポートしています。アプリケーションの要件に応じて適切なプロバイダーを選択することが重要です。

主要キャッシュプロバイダーの比較

プロバイダー 特徴 適用シナリオ クラスタ対応
Ehcache 3 高機能、ディスク永続化対応 一般的なWebアプリケーション 部分的(Terracotta経由)
Caffeine 高速、シンプル、軽量 単一インスタンス、高スループット 非対応
Infinispan 分散キャッシュ、JBoss統合 マイクロサービス、クラスタ環境 完全対応
Redis 外部キャッシュサーバー 大規模分散システム 完全対応

Ehcache 3

Ehcache 3はJCache(JSR-107)標準に準拠した成熟したキャッシュライブラリです。

メリット

  • ヒープ、オフヒープ、ディスクの3層キャッシュをサポート
  • 豊富な設定オプションと監視機能
  • Jakarta EE環境との高い互換性

デメリット

  • クラスタ環境では追加の設定が必要
  • 他のプロバイダーと比較してメモリ使用量が多い傾向

Caffeine

CaffeineはGoogle Guava Cacheの後継として開発された高性能キャッシュライブラリです。

設定例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- pom.xml -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-jcache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>jcache</artifactId>
</dependency>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# application.yml
spring:
  jpa:
    properties:
      hibernate:
        cache:
          use_second_level_cache: true
          region:
            factory_class: jcache
        javax:
          cache:
            provider: com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider

メリット

  • 非常に高速(Window TinyLFUアルゴリズム採用)
  • メモリ効率が良い
  • 設定がシンプル

デメリット

  • クラスタ環境では使用不可
  • ディスク永続化非対応

Redis(Redisson経由)

分散環境でのキャッシュ共有が必要な場合は、Redisの使用を検討します。

設定例

1
2
3
4
5
6
<!-- pom.xml -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-hibernate-6</artifactId>
    <version>3.27.0</version>
</dependency>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# application.yml
spring:
  jpa:
    properties:
      hibernate:
        cache:
          use_second_level_cache: true
          region:
            factory_class: org.redisson.hibernate.RedissonRegionFactory
          redisson:
            config: classpath:redisson.yaml
1
2
3
4
5
# redisson.yaml
singleServerConfig:
  address: "redis://localhost:6379"
  connectionMinimumIdleSize: 5
  connectionPoolSize: 10

メリット

  • 複数アプリケーションインスタンス間でキャッシュを共有
  • 水平スケーリングに対応
  • キャッシュの永続化と高可用性

デメリット

  • ネットワークレイテンシが発生
  • 追加のインフラストラクチャ管理が必要
  • シリアライゼーションのオーバーヘッド

プロバイダー選定のフローチャート

flowchart TD
    A[キャッシュプロバイダーの選定] --> B{クラスタ環境が必要?}
    B -->|はい| C{外部キャッシュサーバーを許容?}
    B -->|いいえ| D{パフォーマンス重視?}
    
    C -->|はい| E[Redis / Redisson]
    C -->|いいえ| F[Infinispan]
    
    D -->|はい| G[Caffeine]
    D -->|いいえ| H{ディスク永続化が必要?}
    
    H -->|はい| I[Ehcache 3]
    H -->|いいえ| G

CacheConcurrencyStrategyの選定基準

CacheConcurrencyStrategyは、複数のトランザクションが同時に同じエンティティにアクセスする際の振る舞いを制御します。

各戦略の詳細

戦略 説明 整合性 パフォーマンス 適用シナリオ
READ_ONLY 読み取り専用。更新不可 完全 最高 マスターデータ、定数テーブル
NONSTRICT_READ_WRITE 緩やかな読み書き。時折の古いデータを許容 弱い 高い 更新頻度が低く、一時的な不整合を許容できるデータ
READ_WRITE 厳密な読み書き。ソフトロックを使用 強い 中程度 一般的なトランザクショナルデータ
TRANSACTIONAL JTAトランザクションと統合 最強 低い XA分散トランザクション環境

READ_ONLY

エンティティが決して更新されない場合に使用します。不変オブジェクトに最適です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
@Immutable  // Hibernateに不変であることを明示
public class Country {
    
    @Id
    private String code;
    
    private String name;
    
    // コンストラクタ、ゲッターは省略(セッターは提供しない)
}

注意点: READ_ONLYでキャッシュされたエンティティを更新しようとすると、UnsupportedOperationExceptionがスローされます。

NONSTRICT_READ_WRITE

更新頻度が低く、一時的なデータの不整合を許容できる場合に使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class UserPreference {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String theme;
    
    private String language;
    
    // ユーザー設定は頻繁に更新されず、一時的な不整合は許容できる
}

動作原理: 更新時にキャッシュエントリを無効化し、次回アクセス時に再読み込みします。ロックを取得しないため、同時更新時に古いデータを読み取る可能性があります。

READ_WRITE

一般的なトランザクショナルデータに推奨される戦略です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private Integer price;
    
    @Version
    private Integer version;  // 楽観的ロックと組み合わせて使用
}

動作原理: ソフトロック機構を使用し、更新中のエンティティへの同時アクセスを制御します。トランザクションがコミットされるまで、他のトランザクションはキャッシュからではなくデータベースから直接読み取ります。

TRANSACTIONAL

JTA(Java Transaction API)環境でのみ使用可能です。完全なトランザクション分離を提供します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
public class BankAccount {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private BigDecimal balance;
    
    // 金融トランザクションでは完全な一貫性が必要
}

注意点: すべてのキャッシュプロバイダーがこの戦略をサポートしているわけではありません。Infinispan JTA CacheはTRANSACTIONAL戦略をサポートしています。

戦略選定のガイドライン

flowchart TD
    A[CacheConcurrencyStrategy選定] --> B{エンティティは更新される?}
    B -->|いいえ| C[READ_ONLY]
    B -->|はい| D{厳密な一貫性が必要?}
    
    D -->|いいえ| E{更新頻度は?}
    D -->|はい| F{JTA環境?}
    
    E -->|低い| G[NONSTRICT_READ_WRITE]
    E -->|高い| H[キャッシュ対象から除外を検討]
    
    F -->|はい| I[TRANSACTIONAL]
    F -->|いいえ| J[READ_WRITE]

クエリキャッシュの設定方法

クエリキャッシュは、JPQLやCriteria APIで実行されるクエリの結果をキャッシュします。エンティティキャッシュとは別に設定が必要です。

クエリキャッシュの有効化

1
2
3
4
5
6
7
# application.yml
spring:
  jpa:
    properties:
      hibernate:
        cache:
          use_query_cache: true  # クエリキャッシュを有効化

クエリへのキャッシュ設定

JPA/Hibernateでは、個々のクエリに対してキャッシュを有効化します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    
    // Spring Data JPAのQueryHintsアノテーションを使用
    @QueryHints({
        @QueryHint(name = "org.hibernate.cacheable", value = "true"),
        @QueryHint(name = "org.hibernate.cacheRegion", value = "query.products.byCategory")
    })
    List<Product> findByCategoryId(Long categoryId);
}

EntityManagerを直接使用する場合は以下のように設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Repository
public class ProductRepositoryCustomImpl {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public List<Product> findActiveProducts() {
        return entityManager.createQuery(
                "SELECT p FROM Product p WHERE p.active = true", Product.class)
            .setHint("org.hibernate.cacheable", true)
            .setHint("org.hibernate.cacheRegion", "query.products.active")
            .getResultList();
    }
}

クエリキャッシュの動作原理

クエリキャッシュは以下の2つのリージョンで構成されます。

  1. default-query-results-region: クエリ結果(エンティティIDのリスト)を格納
  2. default-update-timestamps-region: テーブルの最終更新タイムスタンプを格納
sequenceDiagram
    participant App as アプリケーション
    participant QC as クエリキャッシュ
    participant TS as タイムスタンプキャッシュ
    participant EC as エンティティキャッシュ
    participant DB as データベース

    App->>QC: クエリ実行(キャッシュ有効)
    QC->>TS: テーブルのタイムスタンプを確認
    
    alt キャッシュが有効
        TS-->>QC: タイムスタンプ有効
        QC-->>App: キャッシュされたIDリストを返却
        App->>EC: IDでエンティティを取得
        EC-->>App: エンティティを返却
    else キャッシュが無効または存在しない
        TS-->>QC: タイムスタンプ無効
        QC->>DB: クエリを実行
        DB-->>QC: 結果を返却
        QC->>QC: 結果をキャッシュ
        QC-->>App: 結果を返却
    end
~~~

### クエリキャッシュの注意点

クエリキャッシュには以下の注意点があります。

1. **タイムスタンプリージョンの設定**: `default-update-timestamps-region`には有効期限やサイズ制限を設定しないでください。LRU(Least Recently Used)ポリシーは不適切です

2. **エンティティキャッシュとの併用**: クエリキャッシュはエンティティIDのみを格納するため、エンティティ自体もキャッシュしないとN+1問題が発生する可能性があります

3. **更新頻度の高いテーブル**: 頻繁に更新されるテーブルに対するクエリキャッシュは効果が薄い(すぐに無効化される)

~~~java
// 悪い例: 頻繁に更新されるテーブルへのクエリキャッシュ
@QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
List<Order> findByStatus(OrderStatus status);  // 注文は頻繁に更新される

// 良い例: 更新頻度が低いテーブルへのクエリキャッシュ
@QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
List<Country> findAll();  // 国マスターは滅多に更新されない
~~~

## キャッシュ無効化とライフサイクル管理

キャッシュの適切な無効化は、データの整合性を維持するために不可欠です。

### 自動無効化

Hibernateは、EntityManagerを通じたエンティティの変更を自動的に検出し、関連するキャッシュエントリを無効化します。

~~~java
@Service
@Transactional
public class ProductService {
    
    @Autowired
    private ProductRepository productRepository;
    
    public Product updateProduct(Long id, String newName) {
        Product product = productRepository.findById(id).orElseThrow();
        product.setName(newName);
        // トランザクションコミット時に自動的にキャッシュが更新される
        return product;
    }
    
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
        // 自動的にキャッシュから削除される
    }
}
~~~

### 手動無効化

データベースを直接更新した場合や、外部システムによる変更があった場合は、手動でキャッシュを無効化する必要があります。

~~~java
@Service
public class CacheManagementService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Autowired
    private EntityManagerFactory entityManagerFactory;
    
    /**
     * 特定のエンティティをキャッシュから削除
     */
    public void evictEntity(Class<?> entityClass, Object id) {
        entityManagerFactory.getCache().evict(entityClass, id);
    }
    
    /**
     * 特定のエンティティクラスのすべてのキャッシュを削除
     */
    public void evictEntityClass(Class<?> entityClass) {
        entityManagerFactory.getCache().evict(entityClass);
    }
    
    /**
     * すべての2次キャッシュをクリア
     */
    public void evictAllCache() {
        entityManagerFactory.getCache().evictAll();
    }
    
    /**
     * クエリキャッシュの特定リージョンをクリア(Hibernate固有)
     */
    public void evictQueryCache(String region) {
        Session session = entityManager.unwrap(Session.class);
        session.getSessionFactory().getCache().evictQueryRegion(region);
    }
}
~~~

### ネイティブクエリ使用時の注意

ネイティブSQLクエリでデータを更新する場合、Hibernateはキャッシュを自動的に無効化できません。`@Modifying`アノテーションと`clearAutomatically`オプションを使用するか、手動でキャッシュを無効化してください。

~~~java
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    
    @Modifying(clearAutomatically = true)  // 永続化コンテキストをクリア
    @Query(value = "UPDATE products SET price = price * 1.1 WHERE category_id = :categoryId", 
           nativeQuery = true)
    int increasePriceByCategory(@Param("categoryId") Long categoryId);
}
~~~

~~~java
@Service
@Transactional
public class ProductService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private CacheManagementService cacheManagementService;
    
    public void increasePriceByCategory(Long categoryId) {
        productRepository.increasePriceByCategory(categoryId);
        // ネイティブクエリ後は手動でキャッシュを無効化
        cacheManagementService.evictEntityClass(Product.class);
    }
}
~~~

### TTL(Time To Live)によるキャッシュの有効期限管理

Ehcacheでは、キャッシュエントリに有効期限を設定できます。

~~~xml
<!-- ehcache.xml -->
<cache alias="com.example.domain.Product">
    <expiry>
        <ttl unit="minutes">60</ttl>  <!-- 60分後に期限切れ -->
    </expiry>
    <heap unit="entries">1000</heap>
</cache>

<cache alias="com.example.domain.Country">
    <expiry>
        <tti unit="hours">24</tti>  <!-- 最終アクセスから24時間後に期限切れ -->
    </expiry>
    <heap unit="entries">500</heap>
</cache>
~~~

## よくある誤解とアンチパターン

2次キャッシュの導入時によく見られる誤解とアンチパターンを紹介します。

### アンチパターン1: すべてのエンティティをキャッシュする

~~~java
// 悪い例: 頻繁に更新されるエンティティをキャッシュ
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class AuditLog {  // 監査ログは常に追加されるためキャッシュ効果なし
    // ...
}
~~~

**推奨**: 読み取り頻度が高く、更新頻度が低いエンティティのみをキャッシュ対象にします。

### アンチパターン2: クエリキャッシュの過剰使用

~~~java
// 悪い例: すべてのクエリをキャッシュ
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    
    @QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
    List<Order> findByUserId(Long userId);  // ユーザーごとに結果が異なり、キャッシュヒット率が低い
}
~~~

**推奨**: パラメータが少なく、結果が共通化されやすいクエリにのみキャッシュを適用します。

### アンチパターン3: キャッシュ統計の無視

~~~yaml
# 悪い例: 統計を無効化
spring:
  jpa:
    properties:
      hibernate:
        generate_statistics: false  # 問題の特定が困難に
~~~

**推奨**: 開発・テスト環境では統計を有効化し、キャッシュヒット率を監視します。

~~~java
@Component
public class CacheStatisticsReporter {
    
    @Autowired
    private EntityManagerFactory entityManagerFactory;
    
    @Scheduled(fixedRate = 60000)  // 1分ごとに実行
    public void reportCacheStatistics() {
        SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
        Statistics stats = sessionFactory.getStatistics();
        
        log.info("2次キャッシュ統計: ヒット={}, ミス={}, ヒット率={:.2f}%",
            stats.getSecondLevelCacheHitCount(),
            stats.getSecondLevelCacheMissCount(),
            calculateHitRatio(stats.getSecondLevelCacheHitCount(), 
                            stats.getSecondLevelCacheMissCount()));
    }
    
    private double calculateHitRatio(long hits, long misses) {
        if (hits + misses == 0) return 0.0;
        return (double) hits / (hits + misses) * 100;
    }
}
~~~

### アンチパターン4: 遅延ロードのコレクションキャッシュの誤解

~~~java
// 誤解: コレクションをキャッシュすれば関連エンティティも取得できる
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Category {
    
    @OneToMany(mappedBy = "category", fetch = FetchType.LAZY)
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private List<Product> products;  // キャッシュされるのはProduct IDのリストのみ
}
~~~

**注意点**: コレクションキャッシュには関連エンティティのIDのみが格納されます。関連エンティティ自体もキャッシュしないと、アクセス時にデータベースクエリが発行されます。

### アンチパターン5: @Filterとの併用

~~~java
// 動作しない例: @Filterとキャッシュの併用
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@FilterDef(name = "activeOnly", parameters = @ParamDef(name = "active", type = Boolean.class))
@Filter(name = "activeOnly", condition = "active = :active")
public class Product {
    // @Filterが適用されたエンティティはキャッシュと互換性がない
}
~~~

**理由**: フィルタはセッションレベルで適用されるため、2次キャッシュに格納されたデータにはフィルタが適用されません。フィルタ使用時は、そのエンティティのキャッシュを無効化するか、別のアプローチを検討してください。

## まとめと実践Tips

### 2次キャッシュ導入のチェックリスト

1. **対象エンティティの選定**
   - 読み取り頻度 > 更新頻度
   - 複数トランザクションで共有される
   - データサイズが適切

2. **キャッシュプロバイダーの選択**
   - 単一インスタンス: Caffeine(高速)またはEhcache(機能豊富)
   - クラスタ環境: Redis(外部サーバー)またはInfinispan(組み込み)

3. **CacheConcurrencyStrategyの決定**
   - 読み取り専用: `READ_ONLY`
   - 一般的なCRUD: `READ_WRITE`
   - 緩い一貫性で可: `NONSTRICT_READ_WRITE`

4. **モニタリングの設定**
   - Hibernate統計の有効化
   - キャッシュヒット率の監視
   - メモリ使用量の追跡

### パフォーマンス最適化のTips

1. **エンティティキャッシュを優先**: クエリキャッシュはエンティティキャッシュと組み合わせて使用
2. **適切なTTLを設定**: データの鮮度とパフォーマンスのバランスを考慮
3. **オフヒープメモリの活用**: 大量のエンティティをキャッシュする場合はEhcacheのオフヒープを使用
4. **統計に基づく調整**: キャッシュヒット率が低いエンティティは対象から除外

### トラブルシューティング

| 問題 | 考えられる原因 | 対処法 |
|------|----------------|--------|
| キャッシュが効かない | `@Cacheable`アノテーションの欠落 | エンティティクラスにアノテーションを追加 |
| 古いデータが返される | ネイティブクエリによる更新 | 手動でキャッシュを無効化 |
| OutOfMemoryError | キャッシュサイズが大きすぎる | ヒープサイズを縮小、オフヒープを使用 |
| パフォーマンスが改善しない | 対象エンティティの選定が不適切 | 統計を確認し、対象を見直す |

## 参考リンク

- [Hibernate ORM 6.6 User Guide - Caching](https://docs.jboss.org/hibernate/orm/6.6/userguide/html_single/Hibernate_User_Guide.html#caching)
- [Spring Boot Reference Documentation - Caching](https://docs.spring.io/spring-boot/reference/io/caching.html)
- [Ehcache 3 Documentation](https://www.ehcache.org/documentation/3.10/)
- [Caffeine GitHub Repository](https://github.com/ben-manes/caffeine)
- [Redisson Hibernate Integration](https://redisson.org/glossary/hibernate-second-level-cache.html)
- [Jakarta Persistence Specification](https://jakarta.ee/specifications/persistence/3.2/)