1. 概要

ORM(オブジェクトリレーショナルマッピング)フレームワークなどのデータベース抽象化レイヤーの利点の1つは、基になるストアから取得したデータを透過的にキャッシュする機能です。 これにより、頻繁にアクセスされるデータのデータベースアクセスコストを削減できます。

キャッシュされたコンテンツの読み取り/書き込み比率が高い場合、特に大きなオブジェクトグラフで構成されるエンティティの場合、パフォーマンスが大幅に向上する可能性があります。

この記事では、Hibernateの第2レベルのキャッシュについて説明します。

いくつかの基本的な概念を説明し、いつものように簡単な例ですべてを説明します。 JPAを使用し、JPAで標準化されていない機能に対してのみHibernateネイティブAPIにフォールバックします。

2. 第2レベルのキャッシュとは何ですか?

他のほとんどの完全装備のORMフレームワークと同様に、Hibernateには第1レベルのキャッシュの概念があります。 これはセッションスコープのキャッシュであり、永続コンテキストで各エンティティインスタンスが1回だけロードされるようにします。

セッションが閉じられると、第1レベルのキャッシュも終了します。 これは、同時セッションがエンティティインスタンスと互いに分離して動作できるようにするため、実際には望ましいことです。

一方、第2レベルのキャッシュは SessionFactory スコープであり、同じセッションファクトリで作成されたすべてのセッションで共有されます。 エンティティインスタンスがそのID(アプリケーションロジックまたは内部でHibernateのいずれか、他のエンティティからそのエンティティへの関連付けをロードする場合は eg )でルックアップされる場合、およびそのエンティティに対して第2レベルのキャッシュが有効になっている場合、次のことが起こります。

  • インスタンスがすでに第1レベルのキャッシュに存在する場合、そこから返されます
  • インスタンスが第1レベルのキャッシュに見つからず、対応するインスタンスの状態が第2レベルのキャッシュにキャッシュされている場合、データはそこからフェッチされ、インスタンスがアセンブルされて返されます。
  • それ以外の場合は、必要なデータがデータベースからロードされ、インスタンスがアセンブルされて返されます

インスタンスが永続コンテキスト(第1レベルのキャッシュ)に格納されると、セッションが閉じられるか、インスタンスが永続コンテキストから手動で削除されるまで、同じセッション内の後続のすべての呼び出しでインスタンスがそこから返されます。 また、ロードされたインスタンスの状態は、まだ存在しない場合はL2キャッシュに保存されます。

3. リージョンファクトリー

Hibernateの第2レベルのキャッシュは、実際に使用されているキャッシュプロバイダーを認識しないように設計されています。 Hibernateには、実際のキャッシュプロバイダーに固有のすべての詳細をカプセル化するorg.hibernate.cache.spi.RegionFactoryインターフェイスの実装のみを提供する必要があります。 基本的に、Hibernateとキャッシュプロバイダー間のブリッジとして機能します。

この記事では、キャッシュプロバイダーとしてEhcacheを使用します。これは成熟した広く使用されているキャッシュです。 もちろん、 RegionFactory の実装がある限り、他のプロバイダーを選択できます。

次のMaven依存関係を使用して、Ehcacheリージョンファクトリ実装をクラスパスに追加します。

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>5.2.2.Final</version>
</dependency>

最新バージョンのhibernate-ehcacheについては、こちらをご覧ください。 ただし、 hibernate-ehcache バージョンが、プロジェクトで使用するHibernateバージョンと同じであることを確認してください。 hibernate-ehcache5.2.2.Final を使用する場合は、egです。 ]この例のように、Hibernateのバージョンも5.2.2.Finalである必要があります。

hibernate-ehcache アーティファクトは、Ehcache実装自体に依存しているため、クラスパスにも一時的に含まれます。

4. 第2レベルのキャッシュの有効化

次の2つのプロパティを使用して、L2キャッシングが有効になっていることをHibernateに通知し、リージョンファクトリクラスの名前を付けます。

hibernate.cache.use_second_level_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory

たとえば、 persistence.xml では、次のようになります。

<properties>
    ...
    <property name="hibernate.cache.use_second_level_cache" value="true"/>
    <property name="hibernate.cache.region.factory_class" 
      value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
    ...
</properties>

第2レベルのキャッシュを無効にするには(たとえば、デバッグ目的で)、hibernate.cache.use_second_level_cacheプロパティをfalseに設定します。

5. エンティティをキャッシュ可能にする

エンティティを第2レベルのキャッシュに適格にするために、Hibernate固有の @ org.hibernate.annotations.Cache アノテーションを付け、キャッシュ同時実行戦略[ X207X]。

一部の開発者は、標準の @ javax.persistence.Cacheable アノテーションも追加することをお勧めします(ただし、Hibernateでは必要ありません)。したがって、エンティティークラスの実装は次のようになります。

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Foo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private long id;

    @Column(name = "NAME")
    private String name;
    
    // getters and setters
}

エンティティクラスごとに、Hibernateは個別のキャッシュ領域を使用してそのクラスのインスタンスの状態を保存します。 リージョン名は完全修飾クラス名です。

たとえば、 Foo インスタンスは、Ehcacheのcom.baeldung.hibernate.cache.model.Fooという名前のキャッシュに保存されます。

キャッシングが機能していることを確認するために、次のような簡単なテストを作成できます。

Foo foo = new Foo();
fooService.create(foo);
fooService.findOne(foo.getId());
int size = CacheManager.ALL_CACHE_MANAGERS.get(0)
  .getCache("com.baeldung.hibernate.cache.model.Foo").getSize();
assertThat(size, greaterThan(0));

ここでは、Ehcache APIを直接使用して、 Foo インスタンスをロードした後、com.baeldung.hibernate.cache.model.Fooキャッシュが空でないことを確認します。

Hibernateによって生成されたSQLのロギングを有効にし、テストで fooService.findOne(foo.getId())を複数回呼び出して、[X156X]をロードするためのselectステートメントを確認することもできます。 Foo は1回だけ(最初に)出力されます。つまり、後続の呼び出しでエンティティインスタンスがキャッシュからフェッチされます。

6. キャッシュ同時実行戦略

ユースケースに基づいて、次のキャッシュ同時実行戦略のいずれかを自由に選択できます。

  • READ_ONLY :変更されないエンティティにのみ使用されます(そのようなエンティティを更新しようとすると例外がスローされます)。 とてもシンプルでパフォーマンスが良いです。 変更されない一部の静的参照データに非常に適しています
  • NONSTRICT_READ_WRITE :影響を受けるデータを変更したトランザクションがコミットされた後、キャッシュが更新されます。 したがって、強い一貫性は保証されず、古いデータがキャッシュから取得される可能性のある小さな時間枠があります。 この種の戦略は、結果整合性を許容できるユースケースに適しています
  • READ_WRITE :この戦略は、「ソフト」ロックを使用して達成される強力な一貫性を保証します。キャッシュされたエンティティが更新されると、そのエンティティのキャッシュにもソフトロックが保存され、トランザクション後に解放されます。コミットされます。 ソフトロックされたエントリにアクセスするすべての同時トランザクションは、対応するデータをデータベースから直接フェッチします
  • TRANSACTIONAL :キャッシュの変更は分散XAトランザクションで行われます。 キャッシュされたエンティティの変更は、同じXAトランザクションのデータベースとキャッシュの両方でコミットまたはロールバックされます

7. キャッシュ管理

有効期限とエビクションのポリシーが定義されていない場合、キャッシュは無期限に増大し、最終的には使用可能なすべてのメモリを消費する可能性があります。 ほとんどの場合、Hibernateは、実際に各キャッシュ実装に固有であるため、これらのようなキャッシュ管理の義務をキャッシュプロバイダーに任せています。

たとえば、次のEhcache構成を定義して、キャッシュされるFooインスタンスの最大数を1000に制限できます。

<ehcache>
    <cache name="com.baeldung.persistence.model.Foo" maxElementsInMemory="1000" />
</ehcache>

8. コレクションキャッシュ

コレクションはデフォルトではキャッシュされないため、明示的にキャッシュ可能としてマークする必要があります。 例えば:

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Foo {

    ...

    @Cacheable
    @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    @OneToMany
    private Collection<Bar> bars;

    // getters and setters
}

9. キャッシュされた状態の内部表現

エンティティは、Javaインスタンスとして第2レベルのキャッシュに保存されるのではなく、分解された(水和された)状態で保存されます。

  • Id(主キー)は保存されません(キャッシュキーの一部として保存されます)
  • 一時的なプロパティは保存されません
  • コレクションは保存されません(詳細については、以下を参照してください)
  • 非関連付けプロパティ値は元の形式で保存されます
  • ToOne アソシエーションには、id(外部キー)のみが保存されます

これは、キャッシュモデルが基盤となるリレーショナルモデルを反映する一般的なHibernateの第2レベルのキャッシュ設計を示しています。これにより、スペース効率が高く、2つの同期を簡単に保つことができます。

9.1. キャッシュされたコレクションの内部表現

コレクション(OneToManyまたはManyToManyアソシエーション)がキャッシュ可能であることを明示的に示す必要があることはすでに説明しました。そうでない場合、キャッシュされません。

実際、Hibernateは、コレクションごとに1つずつ、別々のキャッシュ領域にコレクションを格納します。 リージョン名は、完全修飾クラス名とコレクションプロパティの名前です(例: com.baeldung.hibernate.cache.model.Foo.bars )。 これにより、コレクションに個別のキャッシュパラメータ、egの削除/有効期限ポリシーを柔軟に定義できます。

また、コレクションに含まれるエンティティのIDのみがコレクションエントリごとにキャッシュされることに注意することが重要です。つまり、ほとんどの場合、含まれるエンティティもキャッシュ可能にすることをお勧めします。

10. HQLDMLスタイルのクエリとネイティブクエリのキャッシュの無効化

DMLスタイルのHQL( insert update delete HQLステートメント)に関しては、Hibernateはどのエンティティがそのような操作の影響を受けるかを判別できます。

entityManager.createQuery("update Foo set … where …").executeUpdate();

この場合、すべてのFooインスタンスはL2キャッシュから削除されますが、他のキャッシュされたコンテンツは変更されません。

ただし、ネイティブSQL DMLステートメントに関しては、Hibernateは何が更新されているかを推測できないため、第2レベルのキャッシュ全体が無効になります。

session.createNativeQuery("update FOO set … where …").executeUpdate();

これはおそらくあなたが望むものではありません! 解決策は、ネイティブDMLステートメントの影響を受けるエンティティをHibernateに通知して、Fooエンティティに関連するエントリのみを削除できるようにすることです。

Query nativeQuery = entityManager.createNativeQuery("update FOO set ... where ...");
nativeQuery.unwrap(org.hibernate.SQLQuery.class).addSynchronizedEntityClass(Foo.class);
nativeQuery.executeUpdate();

この機能は(まだ)JPAで定義されていないため、Hibernateネイティブ SQLQueryAPIにフォールバックしすぎています。

上記はDMLステートメント( insert update delete およびネイティブ関数/プロシージャ呼び出し)にのみ適用されることに注意してください。 ネイティブselectクエリはキャッシュを無効にしません。

11. クエリキャッシュ

HQLクエリの結果もキャッシュできます。 これは、めったに変更されないエンティティに対して頻繁にクエリを実行する場合に役立ちます。

クエリキャッシュを有効にするには、hibernate.cache.use_query_cacheプロパティの値をtrueに設定します。

hibernate.cache.use_query_cache=true

次に、クエリごとに、クエリがキャッシュ可能であることを明示的に示す必要があります( org.hibernate.cacheable クエリヒントを介して):

entityManager.createQuery("select f from Foo f")
  .setHint("org.hibernate.cacheable", true)
  .getResultList();

11.1. クエリキャッシュのベストプラクティス

クエリキャッシュに関連するガイドラインとベストプラクティスを次に示します。

  • コレクションの場合と同様に、キャッシュ可能なクエリの結果として返されるエンティティのIDのみがキャッシュされるため、そのようなエンティティに対して第2レベルのキャッシュを有効にすることを強くお勧めします。
  • 各クエリのクエリパラメータ値(バインド変数)の組み合わせごとに1つのキャッシュエントリがあるため、パラメータ値のさまざまな組み合わせが多数予想されるクエリは、キャッシュの候補としては適していません。
  • データベースで頻繁に変更されるエンティティクラスを含むクエリも、キャッシュの候補としては適していません。変更されたインスタンスがクエリ結果の一部としてキャッシュされるかどうか。
  • デフォルトでは、すべてのクエリキャッシュの結果はorg.hibernate.cache.internal.StandardQueryCacheリージョンに保存されます。 エンティティ/コレクションのキャッシュと同様に、このリージョンのキャッシュパラメータをカスタマイズして、必要に応じて削除と有効期限のポリシーを定義できます。 クエリごとにカスタムリージョン名を指定して、クエリごとに異なる設定を提供することもできます。
  • キャッシュ可能なクエリの一部としてクエリされるすべてのテーブルについて、Hibernateは最終更新タイムスタンプをorg.hibernate.cache.spi.UpdateTimestampsCacheという名前の別のリージョンに保持します。 クエリキャッシングを使用する場合、この領域を認識することは非常に重要です。これは、Hibernateがこの領域を使用して、キャッシュされたクエリ結果が古くないことを確認するためです。 このキャッシュのエントリは、クエリ結果領域に対応するテーブルのキャッシュされたクエリ結果がある限り、削除/期限切れにしないでください。 とにかく大量のメモリを消費しないため、このキャッシュ領域の自動削除と有効期限をオフにすることをお勧めします。

12. 結論

この記事では、Hibernateの第2レベルのキャッシュを設定する方法について説明しました。 Hibernateはバックグラウンドですべての面倒な作業を行い、第2レベルのキャッシュ使用率をアプリケーションのビジネスロジックに対して透過的にするため、構成と使用はかなり簡単であることがわかりました。

このHibernateの第2レベルのキャッシュチュートリアルの実装は、Github利用できます。 これはMavenベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。