ビデオ

永続化コンテキスト

Ebeanの永続化コンテキストとそのサポートする様々なスコープについて説明します。

定義

EbeanにはJPAのエンティティマネージャはありませんが、永続化コンテキストがあります。実際、一貫性のあるオブジェクトグラフを構築するには、使用に値するORMであれば永続化コンテキストが必要と言っても過言ではありません。

JPA v1.0仕様 - 5.1節

「永続化コンテキストとは、管理されるエンティティインスタンスの集合であり、任意の永続エンティティIDに対して一意のエンティティインスタンスが存在します。永続化コンテキスト内では、エンティティインスタンスとそのライフサイクルはエンティティマネージャによって管理されます。」

Ebeanには、一意のエンティティインスタンスを確保するための永続化コンテキストがありますが、ライフサイクル管理のアプローチはJPAとは異なります。

つまり、Ebeanは一意のエンティティインスタンスを確保するために永続化コンテキストを使用しますが、ライフサイクル管理のアプローチはJPAとは異なります。Ebeanにはエンティティマネージャも、persist/merge/flushなどのライフサイクルメソッドもなく、永続化コンテキストは永続化に関与しません。

一意のエンティティインスタンス

Ebeanは、クエリと遅延ロード(オブジェクトグラフを構築する場合)に永続化コンテキストを使用します。これは、一貫性のあるオブジェクトグラフ(IDごとに1つのインスタンス)が構築されるようにするためです。

一意のインスタンスを確保することは、一貫性のあるオブジェクトグラフを構築することに相当します。

例えば、注文とその顧客のリストを取得する場合、永続化コンテキストによって、特定のIDに対して顧客インスタンスが1つだけ取得されることが保証されます(例えば、「顧客ID=7」のインスタンスを2つ以上持つことは許可されません)。

リレーショナルな結果セットの性質上、オブジェクトグラフをリレーショナルな結果セットから構築する場合、使用に値するORMであれば永続化コンテキストが必要だと言えるでしょう。

例えば、永続化コンテキストがなく、「顧客7」のインスタンスを2つ以上許可した場合、一方のインスタンスを変更してももう一方は変更されず、非常に厄介なことになります。「永続化コンテキスト」は、ユーザー/アプリケーションコードが一意のエンティティインスタンスで動作することを保証します。

永続化

Ebeanはライフサイクル管理に異なるアプローチを取っています。主な違いは、Ebeanでは各Bean自体が独自のダーティチェックを持ち(変更されたかどうかを検出し、楽観的同時実行制御のために古い/元の値を保持する)ことです。

これは、EbeanではBeanを永続化するときに永続化コンテキストを必要とせず、使用しないことを意味します。

Ebeanの永続化コンテキストは永続化に関与しません。

JPAの実装では一般的に、ダーティチェックはエンティティマネージャによって実行されます。エンティティマネージャは一般的に楽観的同時実行制御のために古い/元の値を保持し、Beanは「フラッシュ」(挿入/更新/削除として)されるためにエンティティマネージャに「アタッチ」される必要があります。[注:JDOベースのJPA実装では、少し異なる方法で行われます]。

Ebeanのアプローチの長所

  • エンティティマネージャを管理する必要がない
  • persist/merge/flushなど、アタッチ/デタッチされたBeanよりもsave/deleteがシンプル

Ebeanのアプローチの短所

  • Ebeanは、スカラ型が不変であると仮定しています。String、Integer、Double、Float、BigDecimalなどはほとんど不変ですが、java.util.Dateなど、一部は不変ではありません。これは、Ebeanではjava.util.Dateを変更してもEbeanはその変更を検出しないことを意味します。代わりに、異なるjava.util.Dateインスタンスを設定する必要があります。

スコープ

Ebeanには、永続化コンテキストに3つのスコープがあります - トランザクションスコープクエリスコープオブジェクトグラフスコープ

トランザクションスコープ

Ebeanでは、永続化コンテキストはデフォルトでトランザクションスコープです。これは、新しいトランザクションを(暗黙的または明示的に)開始すると、Ebeanが新しい永続化コンテキストを開始することを意味します。

永続化コンテキストは、トランザクションの終了後も存続します。これにより、トランザクション終了後に発生する遅延ロードは、インスタンスが作成されたものと同じ永続化コンテキストを使用できます。

つまり、永続化コンテキストは

  • トランザクション開始時に開始する
  • トランザクションスコープ中に、すべてのオブジェクトグラフ(クエリ)の構築に使用される
  • トランザクション終了後も存続し、そのオブジェクトグラフに対して発生するすべての遅延ロードも、同じ永続化コンテキストを使用する

トランザクションスコープの永続化コンテキストを「第一レベルキャッシュ」として

永続化コンテキストは、しばしば「第一レベルキャッシュ」またはL1キャッシュと呼ばれます。トランザクションに最も頻繁にスコープされるため、「トランザクションキャッシュ」と呼ばれることも見かけます。

// a new persistence context started with the transaction
Ebean.beginTransaction();
try {
  // find "order 72" results in that instance being put
  // into the persistence context
  Order order = Ebean.find(Order.class, 72);

  // finds an existing "order 72" in the persistence context
  // ... so just returns that instance
  Order o2 = Ebean.find(Order.class, 72);
  Order o3 = Ebean.getReference(Order.class, 72);

  // all the same instance
  Assert.assertTrue(order == o2);
  Assert.assertTrue(order == o3);
} finally {
  Ebean.endTransaction();
}

上記のコードは、「注文72」のインスタンスが1つしかないことを示しています。同じ注文を(単一の永続化コンテキストのスコープ内で)再度フェッチしようとすると、同じインスタンスが返されます。

ただし、通常は、単一のトランザクション内で同じ注文を複数回フェッチするコードは記述しません。上記のコードは、通常記述するようなものではありません。

より現実的な例としては、永続化コンテキストが使用される場合です。

// a new persistence context started with the transaction
Ebean.beginTransaction();

try {
  // find "customer 1" results in this instance being
  // put into the persistence context
  Customer customer = Ebean.find(Customer.class, 1);

  // for this example … "customer 1" placed "order 72"
  // when "order 72" is fetched/built it has a foreign
  // key value customer_id = 1...
  // As customer 1 is already in the persistence context
  // this same instance of customer 1 is used
  Order order = Ebean.find(Order.class, 72);
  Customer customerB = order.getCustomer();

  // they are the same instance
  Assert.assertTrue(customer == customerB);
} finally {
  Ebean.endTransaction();
}

これらの例から、永続化コンテキストは一定の程度キャッシュとして機能することがわかるはずです。オブジェクトグラフを取得して移動する場合、必要なデータベースクエリの数を削減できる場合があります。

ただし、永続化コンテキストの主な機能は、特定のIDに対して...一意のインスタンスを確保することです(オブジェクトグラフを一貫した方法で構築するため)。それが時々キャッシュのように見える/動作するのは、むしろ副次的な効果です。

クエリスコープ

クエリスコープの目的は、データベースから新しいデータを読み取るために、トランザクションスコープの永続化コンテキスト(L1キャッシュ)を効果的にバイパスすることです。

一般的なシナリオは、トランザクションの途中で、特定のクエリがデータベースにヒットして新しいデータを取得することを確認したい場合です。その際、現在のトランザクションスコープの永続化コンテキストの使用を避けたいと考えています。

// transaction with a persistence context
Transaction transaction = ...

// Fetch customer 42 hitting the database
// ... ignoring the transaction persistence context
Customer customer = Ebean.find(Customer.class)
      .setId(42)
      // ignore transaction persistence context
      .setPersistenceContextScope(QUERY)
      .findOne();

オブジェクトグラフスコープ - 大規模なクエリ

findEach()findStream()findIterate()などのストリーミングタイプのクエリを使用すると、Ebeanは永続化コンテキストに特別なスコープを使用します。これらのクエリは、任意の大きさのクエリ要求を反復処理することが予想されるため、すべてのオブジェクトグラフをメモリに保持したくありません。

これらのストリーミングクエリでは、Ebeanは反復処理が発生したときに新しい永続化コンテキストを作成し、スコープは効果的にオブジェクトグラフごとにになります。