Slice による OpenJPA アプリケーションの拡張

Slice は、OpenJPA に含まれる分散永続化のためのモジュールです。Slice を使用することで、単一のデータベースを対象に開発したアプリケーションを、水平パーティショニングされた (場合によっては異種混合の) 分散データベース環境に適応させることができます。しかも、オリジナルのアプリケーション・コードやデータベース・スキーマを変更する必要はありません。Slice ならではの柔軟性を (クラウドや Software-as-a-Service 用のアプリケーションをはじめとする) 皆さん独自のアプリケーションで活かす方法を学んでください。

Pinaki Poddar, Senior Software Engineer, IBM  

Pinaki PoddarPinaki Poddar は、オブジェクトの永続化を重点としてミドルウェア技術に取り組んでいます。彼は Java Persistence API (JSR 317) 仕様の Expert Group のメンバーであり、Apache OpenJPA プロジェクトのコミッターでもあります。過去、国際的投資銀行のためのコンポーネント指向の統合ミドルウェア、医療産業用の医療画像処理プラットフォームの構築に貢献した経験もあります。彼は博士論文で、言語固有のニューラルネットワーク・ベースの自動音声認識を開発しました。


developerWorks 貢献著者レベル

2010年 8月 24日

はじめに

Slice は OpenJPA を水平パーティショニングされた分散データベース環境に拡張します。現在 1 つのデータベースしか使用していない OpenJPA ベースのアプリケーションでも、Slice を使用すればデータを多数のデータベースでパーティショニングしたストレージ環境に合わせて再構成することができます。しかもこのアップグレードには、アプリケーション・コードやデータベース・スキーマの変更は不要です。

データの水平パーティショニングの直接的なメリットは、大量のデータに対するパフォーマンスが向上することです。特に、作業単位 (トランザクション) やクエリーをデータ・セット全体のサブセットに制限することの多いアプリケーション (地理的領域でパーティショニングされたマルチテナント Software-as-a-Service プラットフォームやカスタマー・データベースなど) では、このメリットが顕著に現れます。このようなシナリオで Slice のようなデータ・パーティショニングをベースとしたソリューションが役立つのは、Slice では、すべてのデータベース操作をパーティション全体で並列に実行することによってマルチコア・ハードウェアと I/O バウンドの操作の並行性を有効利用できるだけでなく、データベース・クエリーのターゲットをパーティションのサブセットに絞り込むことも可能だからです。

この記事では、以下の内容について説明します。

  • Slice 対応のアプリケーションを構成する方法
  • Slice がデータをパーティション全体に分散する方法
  • Slice が多数のパーティションからのクエリーの結果を集約またはソートする方法
  • 効率的な並列操作のために満たさなければならないパーティショニングの条件
  • Slice が OpenJPA ランタイムをパーティショニングされたデータベースに適応するように拡張することによって解決されるコア設計/アーキテクチャーの問題

JPA の概要

JPA (Java™ Persistence API) は、リレーショナル・データベースに管理対象オブジェクトを永続化させるための仕様です。JPA の中核的な概念の枠組みには、javax.persistence パッケージの EntityManagerFactory および EntityManager というインターフェースとして具現化された、永続ユニットと永続コンテキストの 2 つがあります。まず、永続ユニットは以下のものを表わします。

  • Java の永続的なデータ・タイプのセット
  • Java の永続的なデータ・タイプのマッピング仕様
  • データベース接続プロパティー
  • プロバイダーに固有な一連のカスタム・プロパティー (該当する場合)

永続コンテキストが表すのは、管理対象となる一連の永続インスタンスです。永続コンテキストはまた、以下のような永続化操作の基本インターフェースでもあります。

  • 新規インスタンスの作成
  • 主 ID によるインスタンスの検索
  • ストリング・ベースまたは動的に構成されるクエリーによるインスタンスの選択
  • トランザクション境界の設定

アプリケーションによってなされる、永続状態のいかなる変異も JPA プロバイダーによってモニターされるという意味では、永続コンテキストがインスタンスを管理します。トランザクションがコミットされた時点、またはコンテキストがフラッシュされた時点で、該当するデータベース・レコードが自動的に更新されます。

JPA が全体的なテーマとして推進しているアプリケーション・プログラミング・モデルでは、永続化操作とクエリーが Java オブジェクト・モデルを参照する一方で、プロバイダーがオブジェクト・モデルとデータベース・スキーマとの間のマッピングを行います。つまり、Java クラスを 1 つ以上のデータベース・テーブルに、Java のデータ・タイプの永続属性をデータベース列に、リレーションを外部キーに変換するなどのマッピングと、Java の new() 演算子を 1 つ以上の INSERT 文に、find()SELECT 文に、永続インスタンスのセッター・メソッドをデータベースに対する UPDATE 文に変換するという、永続化操作から SQL 文へのマッピングの両方を行うのです。

スライスと Slice の違い

この記事では、「スライス」と「Slice」という 2 通りの表記を使用します。「スライス」はデータベースのサブセットを意味し、「Slice」はランタイム OpenJPA モジュールを意味します。

コア JPA 仕様は、永続オブジェクトおよび永続化操作は単一のリレーショナル・データベースにマッピングされるという、暗黙的ながらも確かな前提に基づいています。各永続ユニットは基本的に 1 つのデータベースに関連付けられ、管理対象永続オブジェクトの状態はすべて同じデータベースに保管されます。このような単一のデータベースに関する不可欠の前提を塗り替えるのが、Slice です。Slice では、データ・ストレージ環境が 1 つの巨大なデータベースから一連の水平パーティショニングされたデータベースに変換されても、JPA アプリケーションはそのまま実行し続けることができます。データ・セット全体のサブセットを保管する水平パーティショニングされた物理データベースは、パーティション (partition)、またはシャード (shard)、スライス (slice) などと呼ばれます。

正式に言うと、データ・セット D の水平パーティショニングまたはシャーディング P(D) とは、DN 個の互いに素な集合 Di に分解することです。つまり、以下のような式になります。

D = D1 ∪ D2 ∪ ⋯ ∪ Dn and 
Di ∩ Dn = ∅ for any i ≠ j

Slice は、物理データベースの複数のパーティションまたはシャードを包含するために、仮想データベースを導入して抽象化を行います。Slice 対応アプリケーションでの永続ユニットは、単一の仮想データベースに接続します。そしてこの仮想データベースが、対応する JDBC ドライバーを介してすべての永続化操作を実際の物理データベースに多重化するというわけです。一例として、4 つのスライス (シャード) を使用するように構成された Slice 対応アプリケーションがあるとします。この場合、select c from Customer c where c.age > 20 order by c.age という JPA クエリーを 4 つのスライスのそれぞれで (並列に) 実行すると、スライスごとにソートされた結果がマージされ、さらに仮想データベースによってメモリー内でソートされてから、結果がアプリケーションに表示されることになります。ユーザー・アプリケーションから見れば、仮想データベースのインターフェースが提供する API は単一のデータベースが提供するものと同じです。そのため、単一のデータベース環境からパーティショニングされた分散データベース環境に適応するようになっても、アプリケーション・コードとデータベース・スキーマには何の変更も必要ありません。これは、Slice での仮想データベースによる抽象化が Composite デザイン・パターンに従っているためです。この Slice に備わるシームレスな性質が、Slice を最高に使いやすいものにしています。

特筆すべき点として、パーティションを扱うための手法は他にもありますが、各永続ユニットが別個のシャードで構成されている場合、あるいは単一の永続ユニットが各 EntityManager を 1 つのパーティションに接続するように構成している場合には、それらの代替手法を使用することはできません。

それはなぜかと言うと、JPA 仕様では、永続コンテキストに含まれる管理対象インスタンスはまとめて 1 つのグループとして振る舞わなければならないためです。


Slice の構成方法

パーティショニングされたデータベース環境に対応するように、Slice を使ってアプリケーションをアップグレードするには、アプリケーションを再構成するだけで済みます。このセクションでは、ユーザーが構成できる Slice のプロパティーと、各プロパティーがそれぞれに異なる機能をどのように表現するかという点から Slice を紹介します。Slice を構成するには、あらゆる標準 JPA ランタイムを構成する場合と同じく、META-INF/persistence.xml リソースをクラス・パスが通っているディレクトリーに配置します。META-INF/persistence.xml には永続クラス名やマッピング記述子などの JPA 仕様で定義されたプロパティーの他に、例えば名前と値のペアなどといったプロバイダー固有のプロパティー・セクションが含まれていることがよくあります。Slice 固有のプロパティーについては、プロバイダー固有のプロパティー・セクションに openjpa.slice.* というプレフィックスを付けて記述します。

構成プロパティーは、大きく分けて以下の 3 つのグループに分類することができます。

  • ストレージ環境全体を構成するプロパティー
  • 個々のスライスを構成するプロパティー
  • ランタイムの振る舞いを構成するプロパティー

それでは早速、上記のプロパティーの内容と、それぞれがどのように振る舞いを左右するかについて見ていくことにしましょう。

パーティショニングされたストレージ環境に対応した構成

まず、3 つの Apache Derby データベースにデータをパーティショニングした環境があるとします。この 3 つのデータベースは、それぞれの論理スライス ID (OneTwoThree) によって識別されます。論理スライス ID は人間が読める単純な名前になっており、物理データベースの URL とその他の詳細を (特定の永続ユニットのスコープ内で) 一意に表わします。パーティショニングされたデータベース環境に対応するように Slice を構成するには、リスト 1 に示すような persistence.xml を作成します。

リスト 1. Slice の構成例

リスティングを見るにはここをクリック

リスト 1. Slice の構成例

<persistence-unitname="slice"><properties><propertyname="openjpa.BrokerFactory"value="slice"/><propertyname="openjpa.slice.Names"value="One,Two, Three"/><propertyname="openjpa.slice.Master"value="One"/><propertyname="openjpa.slice.Lenient"value="true"/><propertyname="openjpa.ConnectionDriverName"value="org.apache.derby.jdbc.EmbeddedDriver"/><propertyname="openjpa.slice.One.ConnectionURL"value="jdbc:derby:target/database/slice1"/><propertyname="openjpa.slice.Two.ConnectionURL"value="jdbc:derby:target/database/slice2"/><propertyname="openjpa.slice.Three.ConnectionURL"value="jdbc:some-bad-url"/><propertyname="openjpa.slice.DistributionPolicy"value="acme.UserDistributionPolicy"/></properties>
</persistence-unit>

Slice を起動する

以下は、Slice を起動するために何よりも重要なプロパティーです。

<propertyname="openjpa.BrokerFactory"value="slice"/>

上記のプロパティーが OpenJPA ランタイムに対し、一連の物理データベースで構成される仮想データベースに接続するためだけに特化された永続ユニットを作成するように指示します。このプロパティーは必須です。

各スライスが持つ論理名

次に重要なプロパティーは、論理スライス ID のリストです。

<propertyname="openjpa.slice.Names"value="One,Two,Three"/>

このプロパティー値は、使用可能なすべての論理 ID をカンマ区切りリストに列挙します。論理 ID は、物理データベースの名前と同じであってはありません。論理 ID は永続ユニット内のそれぞれのスライスを一意に表す ID です。例えば、スライスに固有の各構成プロパティー名には、以下のように論理 ID が先頭に追加されます。

<propertyname="openjpa.slice.One.ConnectionURL"value="…"/>

ただし、必ず openjpa.slice.Names プロパティーを使用して論理 ID を記述しなければならないというわけではありません。このプロパティーが指定されていなければ、persistence.xml 全体がスキャンされて、一意に決まるすべての固有論理スライス ID が識別されるからです。けれども論理 ID は明示的に列挙するようにしてください。これについては、後で詳しく説明します。

つのスライスをマスターとして指定する

管理対象インスタンスの主 ID を生成しなければならない場合には、必ずマスター・スライスを使用して主 ID が生成されます。JPA 仕様によると、永続インスタンスごとに永続 ID がなければなりません。永続 ID の値は、アプリケーションで指定することも、データベース・シーケンスによって生成することもできます。後者の場合、マルチデータベース環境のなかでデータベースが生成する主キーの一意性を維持するために、複数のスライスのうち、これらのキーを生成するためのスライスが 1 つ指定されます。指定されたこの特定のスライスは、マスター・スライスと呼ばれます。

スライスをマスターとして指定するには、以下のプロパティーを使用します。

<propertyname="openjpa.slice.Master"value="One"/>

マスター・スライスを明示的に指定することは必須ではありません。上記のプロパティーが指定されなければ、最初のスライスがマスターとして指定されます。ここで重要な意味を持つ言葉は当然、「最初」です。これは、スライスには順序があることを前提とします。スライスの順序は、openjpa.slice.Names を明示的に指定する際のリストによって定義されます。このプロパティーが指定されない場合、スライスの順序は ID の辞書式順序付け (ヒューリスティックであるが明確に決まる順序付け) によって決まりますが、こうした明示的ではないヒューリスティックな順序付けを避けるには、openjpa.slice.Namesopenjpa.slice.Master の両方を明示的に指定することをお勧めします。

各スライスの可用性

複数のデータベースを使用するシナリオでは、1 つ以上のデータベースが使用できなくなることも考えられます。以下のプロパティーは、Slice が 1 つ以上のパーティションと接続できないときの振る舞いを指定します。

<propertyname="openjpa.slice.Lenient"value="true"/>

上記のプロパティーを true に設定すると、Slice は 1 つ以上のスライスに接続できないとしても実行し続けることができます。デフォルトでは、この値は false に設定されます。その場合、構成されたスライスのうち、1 つでも接続可能でないスライスがあると、Slice を起動することはできません。前に記載した例に示されているように、3 番目のスライスは無効なデータベースの URL を指しています。けれどもこのプロパティーを true に設定すれば、2 つのスライスしか有効でないとしても、接続できないスライスは無視されて Slice を起動することができます。

個々の物理データベースを構成する

論理 ID によって識別されるスライスのそれぞれが、物理データベースの URL とその他のプロパティーを指定する必要があります。以下の例に、One として論理的に識別されるスライスに固有の構成を示します。

<propertyname="openjpa.slice.One.ConnectionURL"value="jdbc:derby:target/database/slice1"/>

上記のプロパティーは、One として論理的に識別されるパーティションを、jdbc:derby:target/database/slice1 という URL が設定された Derby データベースのインスタンスに割り当てています。

前述のとおり、スライス固有の構成プロパティー名の先頭には、openjpa.slice.<logical slice identifier> というプレフィックスに続き、オリジナルの OpenJPA プロパティーのキー・サフィックス (ConnectionURL など) が追加されます。この命名規則により、ユーザーは任意の OpenJPA プロパティーを使用して各スライスを独自に構成することができます。その一方、すべてのスライスに共通の構成プロパティーは、単純にオリジナルの OpenJPA プロパティーとして指定することができます。したがって、以下の構成例では、JDBC データベース・ドライバーが共通プロパティーとして指定されて、すべてのスライスに適用されます。

     <propertyname="openjpa.ConnectionDriverName"value="org.apache.derby.jdbc.EmbeddedDriver"/>

注意する点として、Slice では以下のように、同じ構成のなかでの MySQL データベースを表す Four という論理 ID を設定した 4 番目のスライスを指定することも可能です。

リスティングを見るにはここをクリック

<propertyname="openjpa.slice.Four.ConnectionURL"value="jdbc:mysql://localhost/slice4"/><propertyname="openjpa.slice.Four.ConnectionDriverName"value="com.mysql.jdbc.Driver"/>

このような場合、4 番目のスライスでは、共通プロパティーの代わりにスライス固有のプロパティーが使用されます。

実行時の振る舞いを構成する

Slice が主な設計目標としたのは、アプリケーション・コードが 1 つのデータベースを使用する場合とまったく同じになるように、ストレージ環境をカプセル化することでした。その一方、ユーザー・アプリケーションにはアプリケーションで使用されるスライスに関する情報と、ある程度の制御 (例えばアクティブ・スライスの特定のサブセットにクエリーのターゲットを絞り込むなど) が必要になります。アプリケーション・コードに影響することなく、それと同時にある程度の制御を可能にするという、この多少矛盾する目標を実現するため、Slice ではプラグイン・ポリシー・ベースの手法を採り入れています。プラグイン・ポリシーのインターフェースは、ユーザー・アプリケーションによって実装され、構成ファイルの中に記述されます。実行中、Slice はこのユーザー実装をコールバックし、その戻り値を使ってフローを制御します。使用可能なポリシー・ベースの手法には以下のものがあります。

  • データ分散ポリシー — 新しく永続化されたインスタンスを保管するスライスを制御します。
  • 複製ポリシー — 複製されたインスタンスを保管するスライスを制御します。
  • クエリー・ターゲット・ポリシー — 実行するクエリーのターゲットをスライスのサブセットに絞り込みます。
  • ファインダー・ターゲット・ポリシー — 主キー操作を基準とした検索のターゲットをスライスのサブセットに絞り込みます。

このセクションでは、分散データベース環境に関係する Slice の実行時の振る舞いを、上記の構成可能なポリシーの観点から詳細に説明します。

Slice でのデータ分散ポリシー

Slice にはデータベース・スキーマの変更は一切必要ありません。パーティション・ベースの永続ソリューションには、パーティション ID を識別するために特殊な列をデータベース・スキーマに追加しなければならないことがよくありますが、Slice には、そのようなスキーマ・レベルの情報は不要だからです。Slice では永続インスタンスとその元のデータベース・パーティションとの関係は、永続インスタンスの論理名を介して維持されます。この関係は、永続インスタンスが特定のスライスから読み取られるときに確立されます。けれども新しいインスタンスを永続化する際に、Slice は新規インスタンスと関連付けるデータベース・パーティションを決定することができません。そのため、新規インスタンスと関連付けるスライスを指定するのは、アプリケーションの役目となります。アプリケーションが新規インスタンスのスライスを指定する手段は、データ分散ポリシーです。このポリシーは以下のように persistence.xml に構成することができます。

<propertyname="openjpa.slice.DistributionPolicy"value="acme.UserDistributionPolicy"/>

このプロパティーの値は、org.apache.openjpa.slice.DistributionPolicy インターフェースのユーザー実装の完全修飾クラス名を指定します。このインターフェース契約により、ユーザー・アプリケーションは新しく永続化されたエンティティーの論理スライスを決定することができます。

リスト 2. データベース分散ポリシーのインターフェース契約
package org.apache.openjpa.slice;
public interface DistributionPolicy { 
 String distribute(Object pc, List<String> slices, Object context);  
}

以下に、上記で使用されている入力引数について説明します。

  • pc は、永続化対象となるインスタンスです。これと同じインスタンスが、EntityManager.persist(pc) に入力引数として渡されます。
  • slices は、論理スライス ID の不変リストです。このリストには、現在接続不可能なスライスは含まれません。
  • context は、今後の使用に備えて予約された opaque オブジェクトです。現在の実装に関して言えば、このコンテキストは現行の永続コンテキストと同じです。この暗黙的なセマンティクスは、将来も使用できることを保証するものではありません。

この実装は、指定されている論理スライス ID のいずれか 1 つを返す必要があります。

Slice はこのユーザー実装を、永続化するすべてのルート・オブジェクト・インスタンスで呼び出します。ルート・オブジェクト・インスタンスは、アプリケーションが呼び出す EntityManager.persist(Object r) への明示的な入力引数です。重要な点として、単一エンティティーでの明示的な persist(r) 操作により、他の関連するエンティティーが間接的に永続化される結果となる場合があることに注意してください。JPA アノテーション (またはマッピング記述子) は Java 参照関係を修飾して、その関係パスに沿って永続化操作 (persist()refresh()merge()remove() など) をカスケード処理する方法を指定することができます。したがって、インスタンス r が別のインスタンス q に関連し、rq の間の関係としてカスケード・タイプ PERSIST が付けられているとすると、r を永続化する副次作用として q も永続化されることになります。この振る舞いは、遷移的永続化と呼ばれます。

Slice が新しいルート・インスタンス r を永続化する間に行う重大な決定として、r から接続可能な関連エンティティーはすべて同じスライスに保管されます。そのため、分散ポリシーのユーザー実装はルート・エンティティー r に対してしか呼び出されません。Slice は自動的にルート・インスタンスの遷移的クロージャー C(r) を計算し、現在の分散ポリシーに従って、C(r) の各メンバーを同じ r のスライスに割り当てます。このような遷移的クロージャーを同じスライスに割り当てること (コロケーション) が必要なのは、仮想データベースでは物理データベースの結合操作を実行することができないためです。したがって、論理的に関連付けられたレコードが異なるデータベースにあると、関係を EAGER フェッチすることができません。この制約は、コロケーションの制約と呼ばれます。このコロケーションの制約を回避する方法については、後で説明します (例えば、パーティション間で関係を遅延ロードすることもできますし、同じエンティティー・インスタンスを複数のスライスに複製することも可能です)。

ほとんどのアプリケーションでは、データ分散ポリシーがユーザー・アプリケーションによって提供されますが、Slice は、初心者用あるいは実験的プロトタイプ用にそのまま使用できる実装ポリシーをいくつか提供しています。デフォルトで割り当てられるポリシーは、ランダム・スライスをすべての新しいインスタンスに割り当てる既製のポリシーです。

同じエンティティーを複数のスライスに保管する

分散ポリシーは 1 つのスライスにエンティティーを保管するには役立ちますが、コロケーションの制約により、関連するすべてのインスタンスは同じスライスに保管されます。この制約は、特定の共通データ使用パターンにとっては制限がありすぎます (例えば、ティッカー・シンボル (証券コード) や国別コード、あるいは顧客タイプといったマスター・データは、他の多くのデータ・タイプから参照されます)。そのような場合には、参照されるデータ・タイプが複数のスライスに複製されるように指定することができます。複製されるデータ・タイプの名前は、カンマ区切りリストで persistence.xml 構成に列挙します。

<propertyname="openjpa.slice.ReplicatedTypes"value="acme.domain.Foo,acme.domain.Bar"/>

複製されるデータ・タイプのインスタンスを永続化すると、データ分散ポリシーの代わりに複製ポリシーが呼び出されます。複製ポリシーのインターフェースはデータ分散ポリシーと同様ですが、複製されたエンティティーは複数のスライスに保管できるため、戻り型に関しては異なります。

リスト 3. 複製ポリシーのインターフェース契約
package org.apache.openjpa.slice;
public interface ReplicationPolicy { 
 String[] replicate(Object pc, List<String> slices, Object context);  
}

入力引数のセマンティクスはデータ分散ポリシーでの場合と変わらないものの、戻り値には 1 つのスライス ID ではなく、スライス ID の配列が含まれることになります。null の戻り値はすべてのスライスがアクティブであることを意味する一方、空の配列は例外を発生することになります。この場合も Slice は複製されたエンティティーを保管するスライス ID のすべてを追跡し、複製されたインスタンスが変更されると、同じ更新をすべてのスライスに対してコミットします。このことから、複製されたエンティティー・インスタンスは、複数のデータベースにまったく同じようにコピーされる単一の論理エンティティーと見なすことができます。デフォルトの複製ポリシーでは、エンティティーがすべてのアクティブなスライスに複製されます。

複製されたデータ・タイプに関係するクエリーの場合、Slice はスライスからの個別の結果をフィルタリングして複製されたエンティティーを除外します。そのため、'select count(o) from CountryCode o' のような集約クエリーが 複数のスライスから重複する CountryCode インスタンスをカウントして誤った結果を返すという事態にはなりません。

クエリーのターゲットをスライスのサブセットに絞り込む

デフォルトでは、Slice はクエリーをすべてのアクティブなスライスで実行し、その結果を必要に応じてメモリー内に集約します。一方、実行する各クエリーのターゲットをスライスのサブセットに絞り込むことも可能です。ユーザー・アプリケーションは、クエリー・ターゲット・ポリシーのインターフェースを使用して、このようなクエリー・ターゲットの設定を制御することができます。

リスト 4. クエリー・ターゲット・ポリシーのインターフェース契約
package org.apache.openjpa.slice;
public interface QueryTargetPolicy { 
 String[] getTargets(String query, Map<Object,Object> \
params, List<String> slices, Object context);  
}

入力引数の query は JPQL ストリングで、params はクエリーの結合パラメーター値です。この 2 つの引数にはどちらもキーによって索引が付けられます。残りのパラメーターのセマンティクスは、データ分散ポリシーあるいは複製ポリシーと同じです。

戻り値が指定するのは、その特定のクエリーを実行するスライスです。空または null の配列は有効な戻り値ではありません。このインターフェースは、あらゆるクエリーが実行される前に呼び出されます。デフォルトのクエリー・ターゲット・ポリシーはありません。

ファインダーのターゲットをスライスのサブセットに絞り込む

find() の呼び出しには結合パラメーターがないという点を除けば、ファインダー・ターゲット・ポリシーはクエリー・ターゲット・ポリシーとほとんど同じです。リスト 5 に、このポリシーのインターフェースを記載します。

リスト 5. ファインダー・ターゲット・ポリシーのインターフェース契約
package org.apache.openjpa.slice;
public interface FinderTargetPolicy { 
 String[] getTargets(Class<?> cls, Object oid, List<String> slices, Object context);  
}

入力引数の cls は検索対象のエンティティー・クラス、oid は検索対象の永続 ID です。残りのパラメーターのセマンティクスは、他のポリシーと同じです。

戻り値の契約では、クエリー・ターゲット・ポリシーと同じようなセマンティクスを持ちます。

デフォルトのファインダー・ターゲット・ポリシーはないため、find() はデフォルトで、すべてのスライスでインスタンスを検索します。

分散クエリーの実行

Slice は、スライス単位の並行スレッド内でデータベース操作を実行します。これらのスレッドは、永続ユニットごとのスレッドがキャッシュされるプールに保持されます。アプリケーションで並行性の需要が高くなるにつれ、このプールは拡大していきます。実行が完了したスレッドはこのプールに戻されます。

仮想データベースは物理データベースでのクエリーの実行を調整し、メモリー内で個々のクエリーの結果を事後処理して、集約された結果を作成します。この仕組みを詳しく説明するために、これから典型的なクエリーをいくつか紹介します。

メモリー内で事後処理が行われないクエリー

図 1 に単純なクエリーの例として、個々のスライスからの結果をメモリー内で処理する必要のないクエリーを示します。

図 1. メモリー内で事後処理を行う必要のないクエリー
基本的な SELECT クエリーと、3 つのデータベース・スライスのうち 2 つから返された結果を示す図
    select e from Employee e where e.age < 30

このクエリーの述部は各スライスで評価されます。最終的な結果のリストは、個々のスライスから得られる結果のリストを結合したものとなります。この場合、論理スライス ID の順序が意味を持ち、選択された要素の順序は実質的に論理スライス ID の順序によって決定されます。つまり、順序付けは、クエリー・ターゲット・ポリシーによって返されたスライス ID の順序によって決まるか、または前述の明示的なopenjpa.slice.Names によって構成された順序、あるいは暗黙的な辞書式順序によって決まるということです。例えば {slice1, slice2, slice3} という順序になっているとすると、これらの要素はこれと同じ順序で結果のリストに現れることになります。ただし、3 番目のスライスは選択項目を返さないことに注意してください。

図 2 に示す次の例は、ORDER BY 節を使用したクエリーです。このクエリーは以下のコードを使用します。

    select e from Employee e where e.age < 30 order by e.name
図 2. メモリー内でのマージが必要となる ORDER BY 節を使用したクエリー
単純な SQL クエリーと、3 つのデータベース・スライスのうち 2 つから返された結果、そして名前によってソートされた結果を示す図

個々のクエリーの結果はマージされた後、メモリー内でソートされます。Lii 番目のスライスからの順序付きリストだとすると、集約された結果のリスト L は以下の式で表されます。

L = sort(ΣLi)

メモリー内のソート操作は、リスト Li 自体が順序付けされていれば、(ストレージおよび計算という点で) 効率的に実行することができます。

この例で結果として得られるリストは、各スライスから個別にソートされたリストをマージしたバージョンです。したがって、スライスの自然な順序からすると最初のスライスからの「Mary」が先頭にくるはずでも、辞書式の名前の順序付けにより、結果として得られるリストの先頭には「Bill」がくることになります。

トップ N 分散クエリー

図 3 に示す 3 つ目の例では、クエリー結果が上位 N 個の要素に制限されます。

図 3. LIMIT BY によるクエリー
SQL クエリー、3 つすべてのデータベース・スライスから返された結果、そして setMaxResult 関数によって 2 つに絞り込まれた結果を示す図

トップ N クエリーを JPA で実現する方法は、クエリーに ORDER BY 節を組み入れるとともに、結果に制限を設けることです。以下のクエリーは、最年少の従業員 5 名を検索します。

    em.createQuery(“select e from Employee e order by e.age”)
        .setMaxResults(5)
        .getResultList();

分散クエリー環境では、クエリーは各スライスで個別に実行され、マージされたリストの上位 2 つの要素がメモリー内の仮想データベース層で評価されます。メモリー内での上位 N 個の計算でも、要素 x が最終的なリスト L に出現した場合には x が個々のリスト Li のいずれかに含まれているという事実を利用します。

分散環境でクエリーを集約する

A(D) を、データ・セット D で評価される集約演算 (SUM()MAX() など) だとします。

集約演算子は、A(D) = A(R) であればパーティションに対して可換であると定義されます。ここで、R は各パーティションでの A() の評価の集合 (R = {A(Di), i=1N)) です。

図 4 に、パーティションに対する可換性を持つ集約演算を示します。

図 4. 集約クエリー
SQL クエリーが各スライスでの合計値を出し、これらの合計値が最終結果として集約される仕組みを示す図

このクエリーの例は、select SUM(e.age) from Employee e where e.age > 30 です。S が 30 歳以上の従業員の合計年齢を表し、Sii 番目のスライス内での合計を表すとすると、SSi の合計であることは簡単にわかります。したがって、SUM() にはパーティションに対する可換性があります。Slice では、MAX()MIN()SUM()COUNT() など、パーティションとの可換性を持つすべての集約演算を計算することができます。

一般的なすべての集約演算がパーティションに対して可換であるわけではありません。その一例には AVG() がありますが、現在 Slice では、パーティションと可換でない集約クエリーを正しく評価することができます。

トランザクションと Slice

EntityManager が関係してくるトランザクションを制御するには、JTA (Java Transaction API) を使用するか、またはアプリケーションが EntityTransaction API を使ってデータベース・リソースでのリソース・トランザクションにマッピングするという方法があります。JTA EntityManager の場合、JTA トランザクションはリソース・マネージャーに伝播されます (つまり、仮想データベースがリソース・マネージャーとしてトランザクションを物理データベースに中継するということです)。典型的な構成は、3 つの JNDI に登録されたデータ・ソースを使用した JTA トランザクション用コンテナー環境です。

リスト 6. JNDI に登録されたスライスを使用した構成例
<persistence-unit name=”slice” transaction-type=”JTA”>
<property name="openjpa.slice.Names" value="One,Two,Three"/> 
       <property name="openjpa.slice.Master" value="One"/> 
       <property name="openjpa.slice.One.ConnectionFactoryName" 
value="jdbc/slice-ds1"/> 
       <property name="openjpa.slice.Two.ConnectionFactoryName" 
value="jdbc/slice-ds2"/> 
       <property name="openjpa.slice.Three.ConnectionFactoryName" 
                 value="jdbc/slice-ds3"/>
</persistence-unit>

リソースのローカル EntityManager では、適切な二相コミット・プロトコルほどはトランザクションが保証されないとは言え、リソース・マネージャーが物理データベースでのトランザクション・マネージャーとして機能します。リソースのローカル・トランザクションでは作業単位が最初に分析され、管理対象インスタンスは各スライスを対象としたサブセットへとパーティショニングされます。パーティショニングされたサブセットのそれぞれは、対応するデータベースにフラッシュされます。スライスに対するサブセットが空の場合、そのサブセットは無視されます。フラッシュに失敗したデータベースが 1 つでもあると、トランザクション全体がロールバックされます。

コロケーションの制約

前に説明したように、データ分散ポリシーでは、カスケード・タイプとして指定された PERSIST に続くルート・インスタンス r の遷移的クロージャー C(r) を Slice が自動的に計算し、クロージャー全体を 1 つのスライスに保管する方法を規定しています。ここで重要な点は、クロージャーは persist() が呼び出された時点で計算されることです。したがって、persist() の後に追加された関係は、この明示的なクロージャーには含まれません。

コロケーションの制約を確実に満たすためには、ルート・エンティティーをその関係が割り当てられた後に永続化するように注意しなければなりませんが、この事実を利用して故意にコロケーションの制約に違反し、関連インスタンスを別のスライスに保管することもできます。確かな経験に基づく例で、この要点を説明します。

PersonAddress との間にある単純な 1 : 1 の双方向の関係を考えてみてください。Person.address にはカスケード・タイプとして PERSIST が指定されます。マッピングの観点では、Person が関係の所有者です (つまり、PERSON テーブルに ADDRESS テーブルの外部キーが含まれるということです)。

ここで、2 つのスライス (OneTwo) があり、分散ポリシーでは Person の名前が A から M の文字で始まっている場合には Person をスライス One に保管することを規定しているとします。名前がこれらの文字で始まらない場合には、スライス Two に保管されます。同様に、Address の ZIP コードが偶数で終わっている場合には Address をスライス One に保管し、そうでない場合にはスライス Two に保管されます。

以上の単純なルールに従って、Person pAddress a のインスタンスを作成して保管してみましょう。

リスト 7. 相関関係の制約による影響を説明するサンプル・コード
Person p = new Person();
p.setName(“Alan”); // slice One as name starts with letter A
Address a = new Address();
a.setZipCode(12345); // slice Two as zip code is odd digit

em.getTransaction().begin();
p.setAddress(a);
em.persist(p); // relation to address is set before persist
               // Address persisted by transitive persistence
em.getTransaction().commit();

リスト 7 では、Person pAddress a に関連付けられているため、em.persist(p) が呼び出されると、Address aPerson p と同じスライス One に保管されます。分散ポリシーでは、Address a の ZIP コードが奇数で終わっていることから、このインスタンスをスライス Two に保管すると決定するはずですが、このポリシーは Address a に対して呼び出されることはなく、ルート・インスタンスである Person p に対してしか呼び出されません。Slice は、Address aPerson p の遷移的クロージャー内に含まれていることを検出すると、Person p は分散ポリシーによってスライス One に割り当てられることから、Address a にも同じスライス One が自動的に割り当てられるということになるわけです。

リスト 8 には、永続呼び出しの順番が上記とは異なる場合のコードを示します。

リスト 8. コロケーションの制約を迂回する方法を示すサンプル・コード
em.getTransaction().begin();
em.persist(p); // relation to address is not set before persist
p.setAddress(a);
em.persist(a); // a has to be persisted explicitly
em.getTransaction().commit();

分散ポリシーが Person pAddress a に対して個別に呼び出されると、この 2 つのインスタンスはそれぞれ別のスライスに置かれることになります。

このように、故意にコロケーションの制約に違反して、関連するインスタンスを別のスライスに保管することはできますが、このようなストレージ戦略の有用性は限られています。Person とその関連 Address がそれぞれ別のデータベースに保管されていると、実行できる操作が限られてくるからです。例えば、所有側から関係を遅延ロードすることは可能ですが (関係が遅延ロードされる場合、Person.getAddress() は他のデータベースからでも適切なアドレスを取得します)、関係がまとめて先にロード (Eager Loading) される場合や、非所有側からナビゲートされる場合、あるいは 'select p from Person p where p.address.zipcode = 12345' のような結合を必要とするクエリーからナビゲートされる場合には、エラーが発生することになります。


まとめ

データのパーティショニングは、自然なパーティションが存在する場合 (名前による顧客アカウントの区分や、地域別の住所リストなど)、あるいはアプリケーションにとってデータを分離することが好ましい場合 (マルチテナント方式でホストされるプラットフォームなど) をはじめ、大量のデータに対してスケーリングする上で有効な戦略となります。標準 JPA にはシャーディングすなわち分割に対処する効果的な手段がありません。この仕様は暗黙的に、リポジトリーとして単一のデータベースが使用されていることを前提とするからです。Slice は、データ・パーティションやシャーディングをシームレスにサポートするように OpenJPA 実装を拡張しています。他のシャーディング・ソリューションとは異なり、Slice では、パーティショニングを可能にするために既存のデータベース・スキーマに列を追加する必要はありません。Slice を使用した分散およびクエリーのターゲットは、ポリシー・ベースのプラグイン・インターフェースによって指定することができます。これらのプラグインのおかげで、新しいポリシー・インターフェースを追加し、persistence.xml を再構成するということ以外、既存の JPA アプリケーションでコードを変更する必要はまったくありません。

参考文献

学ぶために

製品や技術を入手するために

  • IBM ソフトウェアの試用版を使用して、次のオープンソース開発プロジェクトを革新してください。ダウンロード、あるいは DVD で入手できます。
  • IBM 製品の評価版をダウンロードするか、あるいは IBM SOA Sandbox のオンライン試用版で、DB2®、Lotus®、Rational®、Tivoli®、および WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source, Java technology
ArticleID=548871
ArticleTitle=Slice による OpenJPA アプリケーションの拡張
publish-date=08242010