2011年3月6日日曜日

JEST: OpenJPA での REST

REST (Representational State Transfer) アーキテクチャー・スタイルは、多層システムが事実上、無制限の数のクライアントにまでスケーリングできるように、クライアントとサーバーとの間で通信する方法を定義します。一方、JPA (Java Persistence API) アーキテクチャー・スタイルは、サーバー・サイドのアプリケーションがリレーショナル・データベースに保管されている永続データを操作する方法を定義します。この REST と JPA には、アーキテクチャーに関して共通の特性があります。この記事では、この 2 つのアーキテクチャー・スタイルを多層 Web アプリケーションのための堅牢なエンド・ツー・エンドのアーキテクチャー基盤に統合するソリューション、JEST について説明し、その具体的な実装を紹介します。JEST は、Apache Software Foundation による Java Persistence 2.0 仕様の実装、OpenJPA プロジェクトの一部です (「参考文献」を参照)。

JPA: 単なる API ではありません

JPA は、単なるオブジェクト・リレーショナル・マッピング (ORM) のソリューション、あるいは JDBC (Java Database Connectivity) の代わりにすぎないと誤解されていることがよくあります。もちろん、JPA は Java 言語の API として表現されていますが、この API の背後にあるのは、オブジェクト指向のトランザクション・アプリケーションを対象としたモデル・ビュー・コントローラー (MVC) アーキテクチャー・スタイルです。

記事ではまず、REST と JPA の傑出した機能について説明し、この 2 つに共通する特性を明らかにします。次に、この 2 つのスタイルを連携させる際の技術上および概念上の問題を詳しく取り上げ、JEST がこれらの問題にどう対処するかを説明します。最後のセクションでは、具体的な実装である Java サーブレットに目を向けます。この Java サーブレットは、サーブレット・コンテナーまたはアプリケーション・コンテナーにデプロイすることができ、JEST に付属の Dojo/Ajax クライアントを介して通常の Web ブラウザーからアクセスすることができます。

REST の原則

REST の原則は、World Wide Web で広範に採用されており、World Wide Web が優れたスケーラビリティーを実現できている主な理由の 1 つとして、この REST の原則が用いられていることが挙げられています (「参考文献」を参照)。REST はいくつかの主要な概念をベースにしています。ここではそれらの概念について、図 1 の典型的なクライアントとサーバーとのやりとりの例を通じて詳しく説明します。


図 1. REST スタイルでのクライアントとサーバーとのやりとりの例
クライアントとサーバーとの REST スタイルのやりとりを説明する図 
  • クライアントとサーバーとのやりとりは、双方向のリソース転送のみをベースにしています。これは、プログラミング言語を使った API ベースのやりとりとは対照的です。API ベースのやりとりでは、クライアントはプログラミング言語の変数の引数を渡すことによって、リモート・プロキシーを介してサーバーの関数を呼び出します。
  • リソースは、識別可能で自己完結している概念です。識別可能とは、すべてのリソースを明確に定義された構文で一意に識別できることを意味します。このような ID に最も一般的な表現としては、Web アドレスなどの URI (Uniform Resource Identifier) が挙げられます。
  • クライアントは、サーバー (例えば Google マップなど) のリソース (近所のコーヒー・ショップの住所などの抽象概念) をリクエストします。サーバーはレスポンスとして、リソースの表現 (例えば、JavaScript コードとコーヒー・ショップの場所を示す地図へのハイパーリンクが埋め込まれた HTML 文書) を送信します。
  • レスポンスとして送信される表現に、そこに含まれるすべての参照を解決するための十分な情報が伴っていれば、その表現は自己完結していると言えます。
  • クライアントは表現を受信すると、大抵の場合はその表現をブラウザーにレンダリングします。ここで注目に値する点として、以下の 3 点があります。
    • リソースの抽象概念は、具体的な表現として具現化されます。
    • サーバーは、同じリソースに対し、さまざまな基準 (ユーザーの自然言語など) に基づく異なる表現を使って応答することができます。リクエストには、受け入れ可能な具体的なフォームを指定することもできます。例えば、通常の HTTP リクエストの Accept ヘッダーで MIME タイプを要求するなどです。
    • クライアントは、表現に含まれるすべての参照を解決することもあれば、解決しないこともあります。例えば、ユーザーは表示に使える画面のサイズ次第で、コーヒー・ショップの住所とともに、その場所を示す地図を表示するためのリンクをクリックすることもあれば、クリックしないこともあるからです。
  • 表現をレンダリングする他に、クライアントは例えば HTML フォームを使って、ユーザーが表現を変更したり、新しい表現を作成したりできるようにすることも可能です。その場合、ユーザーとの対話が完了すると、クライアントは新しいリクエストを更新後の表現と併せて送信します。
  • サーバーが変更された表現を受信すると、そのコンテンツを基にして、元のリソースに反映させるか、新しいリソースを作成します。

このようなリソース表現の交換によるクライアントとサーバーとのやりとりを REST に準拠していると見なす条件となるのは (この条件が重要なのですが)、サーバーが最初のレスポンスを送信してから次のリクエストを受信するまで休止するかどうかです。つまり、クライアントからのリクエストが行われてから、次のリクエストが行われるまでの間、サーバーはクライアントとのやりとりをメモリーに保持するために、コンピューティング能力やその他のリソースを一切消費しないことが条件となります (同様に、サーバーのセッション・データを保管するアプリケーションは、REST に準拠していません)。リクエストが行われてから、次のリクエストが行われるまでの間、サーバーがクライアントのコンテキストを保持することはないため、サーバーのレスポンスは、事実上、数限りないクライアントに、スケーラブルに対応することができます。

ACID トランザクションと REST

実際のところ、ステートレスなサーバーは、クライアントがリクエストするのがリソースだけであれば実現しやすいかもしれませんが、クライアントが複数のリクエストで次々と変更された表現を送信し、そのそれぞれをサーバーがリソースにアトミックに適用しなければならないとすると (つまり、ACID (アトミック性、一貫性、独立性、永続性) トランザクションを保証するとなると)、REST スタイルに準拠するのは難しい課題となります。ACID トランザクションはまさにその性質上、短期メモリーと長期メモリーの両方を必要とするからです。

ACID トランザクションでは、以下のことが行われます。

  • 一定期間に行われる複数のクライアント・アクションを 1 つのアトミック (不可分の) 単位にグループ化します。
  • これらの複合アクションの結果が必ず一貫したものになるようにします。
  • これらの複合アクションの結果が、同時に行われる他のクライアントによるアクションとは必ず切り離されるようにします。
  • これらの複合アクションの結果を永続化します。

短期メモリーは、現行の一連のアクション (作業単位 (Unit Of Work: UOW)、またはインフライト・トランザクションとも呼ばれます) を保持します。長期メモリーは多くの場合、永続化されて共有されるメモリー (ハード・ディスク上のデータベースなど) であり、複合アクションの結果を永続化するために必要となります。

したがって、REST スタイルのクライアント・サーバー・アプリケーションのアーキテクトは、以下の重要な質問に答えを出さなければなりません。

  • サーバーが提供する識別可能なリソースは何か?
  • サーバーが生成するリソース表現は何か?
  • ステートレス・サーバーは ACID トランザクションをどのようにサポートするのか?

これらの質問に対する答えは、JPA ベースのサーバー・アプリケーションのコンテキストにあります。この JPA ベースのサーバー・アプリケーションは、コアとなる REST の原則に違反することなく、任意のプログラミング言語で作成された Web クライアントや Web サービスに対応します。

JPA: オブジェクトを永続化するためのモデル・ビュー・コントローラー

アーキテクチャー・スタイルとしての JPA は、永続 Java オブジェクトを対象とした従来のモデル・ビュー・コントローラー (MVC) アーキテクチャーに従います。

  • モデルは、リレーショナル・データベースです。
  • ビューは、正式なインターフェースやスーパークラスを使用しない (以前の Enterprise JavaBeans 仕様で使用していたような)、強い型付けの POJO (Plain Old Java Object) です。
  • コントローラーは、API そのもの、つまり javax.persistence.EntityManager です。

図 2 に、オブジェクトを永続化するための JPA の MVC アーキテクチャーを示します。


図 2. オブジェクトを永続化する MVC としての JPA
オブジェクトを永続化する JPA の MVC アーキテクチャーを説明する図 

図 2 に示されているように、JPA ベースのアプリケーションがモデル (リレーショナル・データベース) へのアクセスおよび表示手段として使用するのは、ビュー (POJO) だけです。新規レコードの挿入や既存のレコードの更新などといったデータベースに対する変更はすべて、コントローラーを介して (EntityManager.persist() または merge() などにより) 行います。

アプリケーションが永続化サービスに JPA を使用するようになると、いわゆる ORM 問題を JPA プロバイダーに任せられるという直接的なメリットがもたらされます。ORM 問題とは、リレーショナル・スキーマと SQL によって制御されるデータベースの領域と、強い型付けの片方向の参照によって支配されるオブジェクト指向の Java 言語の領域との間で変換を行うという複雑な問題のことです。

JPA が ORM と JDBC の組み合わせよりも優れている理由

ミドルウェア・コンポーネントとして JPA を使用すると、ORM 問題を解決することができますが、JPA を提案する主な理由はもっと別のところにあります。なかでも、この記事の話題に最も関連するのは、JPA には分離トランザクションのサポートという強力な機能が備わっていることです。

JPA プロバイダーは、期間 T の間に行われる一連の永続化処理からなる ACID トランザクションを、期間 T 全体を通してデータベース接続を維持することなく保証します。インフライト・トランザクションをメモリーに保持することで、データベース接続を維持する時間枠を狭めることにより、JPA は同時に実行されるユーザー・トランザクションに対してアプリケーションがよりスケーラブルになるようにします (また、商用データベース・サーバーのコストは cn に比例するため、これはコストの削減につながる可能性もあります。ここで、c はデータベース・システムで許容される同時接続数で、n は 1 より遥かに大きいことも珍しくありません)。

しかし JPA での分離トランザクションの概念はさらに徹底していて、JPA はアクセス — 分離 — 変更 — マージからなるリモート・クライアントのトランザクションをサポートします。クライアントがサーバーに永続オブジェクトをリクエストすると、サーバーはこれらの永続オブジェクトを分離オブジェクト・グラフとして送信します。クライアントはこの分離オブジェクト・グラフのコンテンツまたは構造を変更し、変更後のオブジェクト・グラフを別のリクエストでサーバーに返すことができます。サーバーは ACID 準拠のトランザクション保証により、これらの変更をデータベースにコミットすることができます。JPA アプリケーションは、最初のアクセス・リクエストから次の変更リクエストまでの間、分離オブジェクトへの参照を保持しません。

このような分離トランザクションのセマンティクスは、ユーザー・トランザクションの期間が実際のデータベース・トランザクションより遥かに長くなることがある典型的な Web ベースのクライアントや、トランザクションのほとんどが読み取り専用の場合 (つまり、コミットされることのないトランザクション。人気のある多くの Web サイトでは、サイトで購入をする訪問者と閲覧のみの訪問者の標準的な比は 1:200 です) には、さらに重要となってきます。このアクセス — 分離 — 変更 — マージのスタイルは、スケーラブルでコスト効果の高い JPA ベースのアプリケーションを可能にします。このスタイルを適用したアプリケーションは、例えば 5,000 の同時接続 Web クライアントを、トランザクション処理を失うことも、応答時間を低下させることもなく、わずか 10 のデータベース接続でサポートすることができます。

その一方で、アクセス — 分離 — 変更 — マージのプログラミング・モデルは、クライアントとサーバーとのやりとりに関して以下の前提条件を満たさなければなりません。

  • クライアントは Java 言語で作成されていて、リモート・セッション Bean や同様のファサードを介して JPA を呼び出せるようでなければなりません。
  • サーバーは、分離オブジェクト・グラフを、サーバー・サイドで定義される強い型付けの永続 Java オブジェクトのシリアライズ・バイト列として送信します。これらのバイト列を解釈するには、クライアントに永続 Java 型のクラス定義が必要です。
  • 実際のところ、ほとんどの JPA ベンダーは、ベンダーそれぞれの実装クラスへの依存関係 (標準 Java コレクション型のプロキシーなど) をユーザー定義 POJO に組み込むことで、POJO の管理をより効率的に行えるようにしています。これはつまり、クライアントはベンダーのランタイム・ライブラリーにも依存するということです。

それとは対照的に、任意のプログラミング言語で作成されたクライアントをターゲットとする JEST は、クライアントのコンピューティング環境に関する前提条件を最小限にし、最低限ごく普通の Web ブラウザーがあればよいものとします。したがって、JEST に必要となるのは、任意のプログラミング言語で作成されたクライアントが使用できる分離オブジェクト・グラフの表現を生成すること、そしてそれによって REST の原則を JPA のアクセス — 分離 — 変更 — マージのプログラミング・モデルと統合させることです。

JEST によって REST と JPA を統合する方法

REST と JPA を統合する上で鍵となる技術的課題としては、以下のものが挙げられます。

  • 分離された永続オブジェクト・グラフを、任意のプログラミング言語で作成されたクライアントのリソースとして表現する方法
  • RESTful なサーバーのステートレスな性質を犠牲にすることなく、ACID トランザクションをサポートする方法

JEST リソース: カスタマイズ可能な永続クロージャー

JEST のコンテキストでのリソース (RESTful なアーキテクチャーの中心的概念) は、識別可能な管理対象エンティティーからなるカスタマイズ可能な永続クロージャーです (図 3 を参照)。


図 3. カスタマイズ可能な永続クロージャー
カスタマイズ可能な永続クロージャーの概念を説明する図 

管理対象ルート・エンティティー x の永続クロージャーは、管理対象エンティティーのセット、C(x) ={e} として定義されます。ここで、e  x から直接到達できる場合も、永続関係によって間接的に到達できる場合もあります。さらに当然のことながら、エンティティーは常にエンティティー自体に到達することができます。

永続クロージャーのカスタマイズとは、実行時にルート・エンティティーから到達可能なエンティティーを動的に構成することです。

JPA では、オブジェクトの関係を永続化の特性によって修飾することを規定しています。例えば、Department とその Employee (つまり、List<Employee> employees として宣言された、Department クラス内の private フィールド) との間の多値関係を 1 対多の関係として修飾するには、@OneToMany アノテーションを使用することも、付随するマッピング記述子を使用することもできます。この場合、Department d のすべての Employee インスタンスは、d から到達可能になります。クロージャーには、間接的な関係のパスを含めることもできます。Employee e ごとに @OneToOne アノテーションが付いた Address インスタンス a への参照がある場合、Addressインスタンス a は、Department インスタンスから Employee e を介して間接的に到達できるため、d のクロージャーには Address インスタンス a も含まれることになります。

ただし、重要な点は、永続クロージャーは管理対象エンティティーにのみ適用されることです。したがって、例えば Department インスタンス d が 20 の Employee インスタンスのセットと論理的に関係しているとしても、その関係はロードされていないことがあります。つまり、d を保持する永続コンテキストに、データベースから Employee インスタンスが取り込まれていない可能性があるということです。その場合、d のクロージャーには Employee インスタンスが含まれません。また、クロージャーの評価の副次作用として、これらのインスタンスがデータベースからロードされることもありません。

現行の JPA 仕様では、クロージャーをカスタマイズするための基本機能を提供しています。あらゆる永続プロパティー (関係および基本フィールド) は、FetchType.LAZY または FetchType.EAGER で修飾することができます。このように修飾すると、どのプロパティーあるいは他のインスタンスをロードすればよいかを判断する目的でルート・エンティティー・インスタンスがデータベースから永続コンテキストにロードされる際に、修飾した内容が適用されます。現行の JPA 仕様での主な制約は、このようなカスタマイズが設計時に静的に定義されることです。そのため、実行時にクロージャーを構成することができません。

OpenJPA は、その FetchPlan インターフェースによって永続クロージャーの動的な構成をサポートします (「参考文献」を参照)。アプリケーションで FetchPlan の充実した構文とセマンティクスを使用すれば、find() を実行した結果として検出されたルート・インスタンスに対しても、クエリーで選択されたインスタンスに対しても、クロージャーを構成することができます。JEST はこれらの構造を利用して、関連するエンティティーおよびそれぞれのプロパティーのセットにアクセスします。フェッチ・プランは、クライアントが同じ論理リソースに対して要求する表現のコンテンツを状況に応じて制御できるため、有用な構成体と言えます。例えば、クライアントは情報を携帯電話にレンダリングするか、またはハイエンドのデスクトップ・モニターにレンダリングするかによって、同じコーヒー・ショップをリクエストで要求した際に受信される表現をカスタマイズすることができます。

自己完結した JEST 表現: XML と JSON

JEST の統合方式で次に取り上げる側面は、リソースの表現です。この表現は、任意のプログラミング言語で作成された、ドメインにとらわれないクライアントをターゲットとしています。JEST では、リソース表現に対して以下の性質を規定しています。

JEST と JAX-RS の比較

最初の制約を JAX-RS (Java API for RESTful Web Services) と比較してみると、JAX-RS では永続 POJO を表現するにはアノテーションを付けなければなりません。永続 POJO クラスはコンパイル単位の一部であるため、アノテーションを付けると、ユーザーがアプリケーションを再コンパイルしなければならなくなります。さらに、このようなアノテーションは (JPA と同じく) Fetch.LAZY またはFetch.EAGER として静的に定義されるため、使用するごとにクロージャーの構成をカスタマイズするには不便です。

  • 修飾子が適用されないこと: リソースを表現する際に、いかなる類のアノテーションも修飾も必要としないようでなければなりません (囲み記事「JEST と JAX-RS の比較」を参照)。
  • クライアントで使用されるプログラミング言語を問わないこと: あらゆるプログラミング言語をベースとするクライアントで使用可能な表現でなければなりません。クライアントとサーバーとのやり取りは API ではなく、URI に基づいて行われるため、あらゆる言語で作成されたクライアントが、HTTP リクエストによって表現を受信することができます。また、任意のクライアントが表現のコンテンツを解釈できるように、表現は周知のフォーマット (XML や JSON など) に従います。どのクライアントでも、JEST によって生成された表現を解釈するためにプラグインが必要になることはありません。
  • ドメインにとらわれないこと: 元のリソースが強い型付けをされた永続 Java オブジェクトであるとしても、JEST で生成された表現を使用するために、クライアントに永続クラスの定義が必要になることはありません。REST という意味で、表現は自己記述的です。

ドメイン駆動型の表現と、ドメインによらず不変の表現との違い

任意のプログラミング言語で対応可能な表現とは、暗黙的なフォーマットを持つ、バイトまたは文字の順序付きシーケンスのことです。その一般的な例としては、XML と JSON が挙げられます。このようなストリング・ベースの表現は、クライアントがバイトまたは文字セットの文字のシーケンスを構文解析できること以外には、クライアントが何か特別な機能を持たなくてもよい前提になっているため、広範なクライアントが対象となります。JEST が生成する表現は、XML および JSON フォーマットです。JSON の場合には、いくつかの必須機能で拡張された表現になります。

JEST 表現の中心となるテーマは、JEST がドメイン駆動型ではなく、モデル駆動型であるということです。Java 型の XML 表現がドメイン駆動型であることは珍しくありません。例えば、リスト 1 の単純な永続 Java 型を見てください。


リスト 1. 単純な永続 Java 型
@javax.persistence.Entity public class Actor {   @javax.persistence.Id   private long id;   private String name;   private java.util.Date dob; } 

リスト 2 に、上記に対応する Actor インスタンスの XML 表現を記載します。


リスト 2. Actor インスタンスを表現するドメイン駆動型 XML スキーマ
<?xml version="1.0" encoding="UTF-8"?> <Actor>   <id type="long">1234</id>   <name type="string">Robert De Niro</name>   <dob type="date">Aug 15, 1940</dob> </Actor> 

リスト 2 の XML 表現は、その暗黙的または明示的スキーマが永続ドメイン・クラス Actor の型構造によって制御されるという点で、ドメイン駆動型です。その一方、同じインスタンスをモデル駆動型で表現すると、リスト 3 のようになります。


リスト 3. モデル駆動型 XML スキーマで表現された単純なエンティティー
<?xml version="1.0" encoding="UTF-8"?> <instances>   <instance type="Actor" id="Actor-1234">     <id name="id" type="long">1234</id>     <basic name="name" type="string">Robert De Niro</name>     <basic name="dob" type="date">Aug 15, 1940</dob>   </instance> </instances> 

モデル駆動型の表現には、ドメインによらず不変のスキーマがあることから、あらゆる永続タイプを表現できるだけの汎用性があります。モデル駆動型の表現では Actor のスキーマは変わりませんが、ドメイン駆動型の表現では、その限りでありません。モデル駆動型の表現には、受信側が、受信した情報の正確な構造とは関係なく、一般的な方法で表現を解釈できるというメリットがあります。

JEST-instance.xsd というモデル駆動型スキーマは、JPA 2.0 で導入された Metamodel API から直接派生しています (「参考文献」を参照)。Metamodel API は、Java 言語の型に対する Java Reflection API と似ていますが、永続型をより表現豊かにするために拡張されています。JEST は (いわゆるメタ・メタデータの) javax.persistence.metamodel.Entity または SingularAttribute などの JPA Metamodel 構成体を XML スキーマで表現します。永続エンティティーに対するすべての JEST レスポンスは、同じ JEST-instance.xsd スキーマに従います。

永続クロージャーによる自己完結した表現

永続クロージャーは閉集合です。つまり、クロージャー C(x) に含まれる任意の e1 に対して、以下のことが当てはまります。

e1 がエンティティー e2 を参照する場合、e2 は同じクロージャー C(x) に含まれている必要があります。

このような閉集合特性により、JEST リソースの表現は自己完結しています。これは、REST スタイルの表現が持つ重要な側面です。クライアントが受信する表現に含まれるすべての参照は同じ表現の中で解決されるため、クライアントは別のサーバー・リクエストを行わなくても、すべての参照を解決することができます。

JEST が JSON を拡張している理由

XML 表現では、JEST はドメインによらず不変の XML スキーマを ID 用および参照用にそれぞれ xsd:ID 型、xsd:IDREF 型として定義しています。したがって、標準 XML パーサーは DOM (Document Object Model) 要素への参照を解決することができます。

一方、JSON での自己完結した表現には、技術的な問題があります。それは、Web ブラウザー環境でよく使用されている標準の JSON エンコード・ライブラリーは、循環参照 (「参考文献」を参照) をサポートしないことです (循環参照は、どの永続オブジェクト・グラフでもほぼ一般的に発生することを考えると、これは驚くべき事実です)。既存の JSON エンコーダーに伴うこの制約に対処するため、そしてさらに重要なことに、表現の中核となる前提を自己完結させて、クライアントとサーバーが頻繁にやりとりしなくても済むようにするため、JEST では循環参照をサポートする JSON エンコーダーを提供しています。

JEST の JSON エンコーダーは、$id および $ref という追加プロパティーを導入しています。この 2 つのプロパティーに指定されるのは、オブジェクト・グラフに含まれる可能性のある循環参照を解決するための管理対象エンティティーの永続 ID です。Dojo または Gson で使用可能な標準 JSON パーサーは、この拡張表現を解釈することができるため、これらの追加 $id および $ref プロパティーを単に無視します。JEST だけが、循環参照を使用したオブジェクト・グラフを再作成するために、拡張 JSON 表現を再解釈します。

分離トランザクション: REST と JPA トランザクションを統合させる鍵

トランザクション Web アプリケーションにとって、REST が規定する「リクエストが行われてから次のリクエストが行われるまでサーバーが休止する」というセマンティクスの難題に対処する鍵となるのは、JPA の分離トランザクション・モデルです。JEST は、図 4 に示すシーケンスでトランザクションを行うことにより、JPA と REST のアーキテクチャー・スタイルを統合します。


図 4. REST と JPA の統合を目的とした分離トランザクション
JEST の分離トランザクションのシーケンスを説明する図 
  1. t1 の時点で、リソースに対するクライアント・リクエスト R1 がサーバーに到達します。
  2. サーバーは JPA を使用して、Java オブジェクトのセットとしてデータベースに保管されたリソースにアクセスします。このアクセスには、データベース接続が必要です。アクセスが完了すると、JPA プロバイダーがデータベース接続を解放します。
  3. プロバイダーは通常、これらのオブジェクトを短期メモリー領域に保持します。このメモリー領域は、JPA の用語体系では永続コンテキストと呼ばれます。ただし、このシーケンスでは、プロバイダーは即時に永続コンテキストからオブジェクトを分離して、クライアントで解釈可能な表現 A1 に変換します。そして A1 をオリジナルのリクエスト R1 に対するレスポンスとして送信します。このように即時に分離することにより、レスポンスが送信された後は、サーバー・アプリケーションにリソースが保持されていないようにします。
  4. クライアントが A1 をレンダリングし、ユーザーがその表現を A1' に変更できるようにします。
  5. クライアントはさらなるリソースにアクセスするために、さらにリクエスト R2 と R3 を送信することができます。すると、それぞれに対するレスポンスとして A2、A3 が受信されます。
  6. クライアントはユーザーが A2 または A3 も変更できるようにします。
  7. ユーザーが受信したすべてのレスポンスの変更を完了すると、クライアントは t2 の時点で、変更されたすべての表現 (A1'、A2'、A3') と併せてリクエスト R4 を送信します。
  8. サーバーは受信した変更後の表現を Java オブジェクトに変換し、これらの Java オブジェクトを JPA を使用して永続コンテキストにマージします。この時点で、変更に一貫性があることを検証するために、JPA プロバイダーにはデータベース接続が必要になることがあります。例えば、t1 から t2 までの期間に、他のクライアントが A1'、A2'、A3' に含まれるオブジェクトに矛盾する変更をコミットしていないことを確認するためです。変更が一貫していて、他と分離されていれば、最終的にサーバーは JPA API を介して変更をデータベースにコミットします。

この REST と JPA の統合方式では、クライアントが表現を変更している間、JPA アプリケーションは分離された永続オブジェクトを短期メモリー (永続コンテキスト) に保持しません。また、トランザクションの保証を確実にするために t2 の時点までデータベース接続を維持することもありません。サーバーは、データベース接続と短期メモリーに関する限り、ステートレスであり続けます。サーバーがオンデマンドでこれらのリソースを必要とする時間は、t1 から t2 までの時間 (クライアントの視点から見たトランザクションの合計時間) より遥かに短くなります。

この統合方式は、ステートレス・サーバーがクライアントのコンテキストを保持することも、ACID トランザクションの保証を犠牲にすることもなく、クライアントのトランザクションをサポートするにはどうすればよいのかという重大な問題を解決します。

JEST over HTTP

冪等リクエストとトランザクション・リクエストの比較

HTTP 用語では、リクエストは冪等リクエストであるか、非冪等リクエストであるかのいずれかとなります。JEST のコンテキストでは、HTTP リクエストの有用な分類は、リクエストによって生じる JPA による処理でデータベース・トランザクションが必要になるかどうかです。冪等な処理とは、複数回実行されても、同じ入力に対しては同じ結果となる処理のことです。HTTP の GET 動詞がその一例で、静的 HTML ページなどのリソースに対して、クライアントからのリクエストを何度行っても同じページが取得されます。JEST のコンテキストでは、HTTP リクエストが実質的に JPA による処理に変換されますが、厳密に言うと冪等なリクエストはありません。PurchaseOrder 2345 に対してリクエストを連続して 2 回行ったとしても、同じ結果になることは保証されていません。2 回目のリクエストが行われるまでにデータベース内でPurchaseOrder 2345 のステータスが変更される可能性があるためです。HTTP の PUT も同じく冪等操作ですが、JEST のコンテキストでは、ペイロードがまったく同じ 2 つの連続した PUT は、主キーが重複するという例外を引き起こすため、2 回目のリクエストは失敗することになります。

最後に理解しなければならない JEST の概念は、JEST が通信プロトコルと URI スキームを指定する方法です。REST がクライアントとサーバーとの間の特定の通信プトロコルを直接参照することはありません。しかし、実際には、REST は HTTP と密接な関係を持ちます (そして REST は HTTP の進化に大きく影響してきました)。それが、JEST プロジェクトでは HTTP を JEST の通信プロトコルとして選んでいる理由です (REST の原則に従い、JEST のコアの概念、つまり管理対象エンティティーの永続クロージャーをリソースとし、表現を任意のプログラミング言語で対応可能な、ドメインにとらわれないものにするという概念は、HTTP とは関係のない概念です)。

JEST の URI 構文

JEST は標準 URI を解釈して永続リソースを識別します。標準 URI は、スキーム、権限、パス、クエリーという 4 つのセグメントで定義されます (「参考文献」を参照)。JEST はそのうちパスとクエリーのセグメントに特殊な解釈を適用して、適切な永続化操作に変換します。

いくつかの典型的な JEST URI を用いて、JEST が機能するために URI にエンコードされていなければならない情報を説明します。

まず、以下の URI は主キー m1 を持つ Actor を取得します。

http://openjpa.com:8080/jest/find/plan=basic?type=Actor&m1

以下の URI は、JPQL (Java Persistence Query Language) クエリーを使用して、名前 John を持つ単一の Actor を取得します。

http://openjpa.com:8080/jest/query/single?q=select p from Actor p where p.name=:n&n=John 

JEST URI には、以下の詳細を提供できるだけの十分な情報をエンコードする必要があります。

  • JPA による処理: JEST コンテキストでの永続操作のほとんどは、javax.persistence.EntityManager インターフェースの以下のメソッドです。
    • find(): 主要な ID を基準にエンティティーを見つけます。
    • persist(): 指定された新規エンティティーをデータベース内に作成します。
    • merge(): 現在の永続コンテキストを、指定されたエンティティーの最新の状態で更新します。
    • remove(): 指定された新規エンティティーをデータベースから削除します。

    EntityManager は、JPQL クエリー・ストリングから実行可能 javax.persistence.Query インスタンスを作成するファクトリーとしての役割も持ちます。

    ターゲットの永続化処理は、理想的にはHTTP 動詞に暗示されているはずです。HTTP 動詞の PUTPOSTDELETE はそれぞれ、JPA 操作の merge()persist()remove() に直接対応します (図 5 を参照)。



    図 5. HTTP 動詞から JPA による永続化処理へのマッピング
    HTTP 動詞から JPA による永続化処理へのマッピングを説明する図 

    ただし、HTTP GET については、検索またはクエリー処理に多重化されます。この 2 つの処理の他、HTTP GET はメタモデル (/domain) を取得する場合にも、永続化単位の構成プロパティー (/properties) を取得する場合にも使用されます。GET によって取得可能なリソース・タイプの多様性に対応するためには、JEST ではアクションの名前が URI にエンコードされていること、そして呼び出しコンテキストに続く最初のパス・セグメントをアクションにすることを要件としています。

  • 修飾子: 永続化処理は、さらに制限または修飾することができます。例えば、1 つの結果だけを返すようにクエリーを制限したり、あるいは最初の 20 の結果のみを選択して返すように制限したりするなどです。検索またはクエリー処理については、フェッチ・プランを適用するように修飾し、データベースからフェッチするエンティティーのセットをカスタマイズすることができます。

    修飾子は、パス・セグメント内のアクションの後にエンコードされます。アクションは、ゼロまたは複数の修飾子を持つことができます。修飾子の出現順は重要ではありません。修飾子は、= 文字で区切られたキーと値のペアとして表現されます。ブール値を持つ修飾子の場合には、簡潔にするために値の部分を省略しても構いません。ブール値の修飾子の例には、single (クエリーが 1 つのエンティティーのみによって満たされなければならないことを指定) または named (指定されたクエリー引数が、JPQL ストリングではなく、宣言された NamedQuery の名前を参照することを指定) があります。

  • 引数: 各処理には引数が必要です。例えば find() 処理には、エンティティーのタイプと主要な ID が引数として使用されます。クエリー処理の引数は、事前定義された名前付きクエリーの JPQL ストリングまたは名前です。パラメーター化されたクエリーは、バインディング・パラメーターとして引数の変数も取ります。

    JPA による処理への引数は、HTTP GET リクエストの場合には URI のクエリー部分にエンコードされ、PUTPOST、および DELETE リクエストの場合にはペイロードにエンコードされます。さらに、GET リクエスト URI のクエリー部分にエンコードされる引数は、アクションによって異なります。例えば、find() 処理が取る引数は、検索するエンティティーの Java クラスとその永続 ID の 2 つです。

    引数をクエリー・セグメントにエンコードする理由

    JEST では、JPA による処理の引数を GET リクエスト URI のクエリー・セグメントにエンコードします。その理由は、JEST URI は、JPQL クエリー・ストリングを指定するために使用できるからです。JPQL クエリー・ストリングには、URI のパス・セグメントでは許可されない文字を含めることができます。

    URI のクエリー・セグメントでは、各引数がアンパサンド (&) 文字で区切られ、引数のそれぞれは等号 (=) 文字で区切られたキーと値のペアとして表現されます。修飾子とは異なり、引数には順序が付けられていて、そのほとんどは必須です。

    URI を実行可能な JPA による処理に変換する際の重要な側面は、ストリング・ベースの引数を、JPA による処理に必要な強い型付けの引数に変換することです。例えば、引数 type=Actor Actor.class に変換する必要があります。また、POST リクエストの XML/JSON ペイロードは、強い型付けの Java オブジェクト・グラフに変換してからでないと、永続コンテキストにはマージすることはできません。

JEST の動作

JEST の統合方式の概念および仕組みの基礎をひととおり理解したところで、今度は具体的な実装である JESTServlet を見て行きます。JESTServlet は、リモート・クライアントから JEST の機能にアクセスするための標準的な javax.sevlet.HttpServlet です。JESTServlet は、Tomcat、WebSphere®、Glassfish などの任意の標準サーブレット・コンテナーにデプロイすることができます。

JEST をデプロイする

JESTServlet は OpenJPA インストールにパッケージ化されています。ダウンロード、ビルド、デプロイの手順は、OpenJPA ドキュメント・サイト (「参考文献」を参照) で説明されているので、ここで繰り返すことはしません。代わりに、JESTServlet がサポートする 2 つのデプロイメント・モード、主デプロイメントと補助デプロイメントについて説明します (図 6 を参照)。


図 6. JESTServlet の主デプロイメント・モードと補助デプロイメント・モード
JESTServlet の主デプロイメント・モードと補助デプロイメント・モードを説明する図 

主デプロイメント・モードでは、永続化単位の EntityManagerFactory  JESTServlet 自体がインスタンス化します。名前付き永続化単位は、Web アーカイブにパッケージ化された標準永続化記述子、META-INF/persistence.xml によって記述されます。

補助デプロイメント・モードでは、JESTServlet は独自の永続化単位を持ちませんが、兄弟コンポーネントの永続化単位を検出します。兄弟コンポーネントとは、同じデプロイメント・モジュールにデプロイされた別のサーブレットなどの成果物のことです。補助デプロイメント・モードでの JESTServlet は、兄弟コンポーネントが使用する永続化単位の名前を認識していなければなりません。現在、兄弟コンポーネントは OpenJPA の EntityManagerFactory のネイティブ・プールを起動することになっているので、JESTServlet はこのプールから同じ永続化単位へのハンドルを取得することができます。アプリケーション・コンテナーおよびサーブレット・コンテナーは、おそらくそれぞれ独自の方法に頼って永続化単位 (あるいはそのプロキシー) をプールし、プールした永続化単位を依存性注入という方法で運用コンポーネントに注入することがよくあります。この記事を執筆している時点で、JESTServlet はこれらのコンテナーが管理するプールから永続化単位を見つける方法を一般化していませんが、プロジェクトではそのような機能を追加する方法を調査しているところです。

最初から同梱されているサンプルには、補助デプロイメント・モードで、永続化単位をインスタンス化する単純なサーブレットがJESTServlet とともに同じ Web アーカイブにデプロイされる様子が説明されています。サンプルを見ると、JESTServletがその兄弟コンポーネントの永続化単位についての予備知識なしに、永続化ドメインをブラウズしたり、その汎用のドメインにとらわれない機能によって永続インスタンスを表現したりする方法がわかります。

JEST の Dojo クライアント

JEST は、リソース表現のレンダリングについては意図的に対処していません。例えば、JEST では HTML での表現をサポートしていませんが、その一方、JESTServlet が JavaScript を組み込んだ単一の HTML ページを提供します。このページは実質的に、JEST 機能に対する Ajax クライアントです。このクライアントは Dojo JavaScript ライブラリーを利用し、リソースに対する非同期リクエストを JEST URI 構文を使って JESTServlet に送信します。そして、受信した形のままレスポンス (XML または JSON フォーマット) をレンダリングするか、Dojo のビジュアル・ウィジェットという形でレンダリングします (これは、循環グラフに対処するために特化された JSON エンコーダーの出力は、標準 JSON パーサーで使用可能であるという私の主張を立証しています)。図 7 に、このページを示します。


図 7. JEST に対する Dojo ベースのユーザー・インターフェース
Web ブラウザー上で実行中の JEST に対する Dojo ベースのユーザー・インターフェースのスクリーン・ショット 

図の全体を表示するには、ここをクリックしてください。

ユーザーはこの単一のインタラクティブな Web ページから、使用可能なすべての JEST リソース (永続ドメイン・モデル、永続エンティティー、そして永続化単位の構成) にアクセスすることができます。このページは、ユーザーがリクエストを作成するための HTML フォームを表示します。これらのフォームには、ユーザー指定の修飾子および引数によって、ステップ・バイ・ステップで JEST URI 構文を組み立てる方法が示されます。

JESTServlet は、HTTP サーブレットのリクエスト・レスポンスを永続コンテキストにバインドするコンテキストで、各クライアント・リクエストを実行します。コンテキストの有効期限は、リクエスト・レスポンス・サイクルの有効期限と同じなので、リクエストが行われてから次のリクエストが行われるまで、REST のステートレスな特性が満たされるというわけです。

まとめ

JEST が利用している JPA アーキテクチャーの充実した機能概念は、以下のとおりです。

  • 強力な問い合わせ言語
  • カスタマイズ可能なクロージャー
  • 汎用ドメイン・モデル
  • 分離トランザクション。任意のプログラミング言語で作成されたリモート・クライアントを対象とした、コンテンツがリッチで動的に構成可能な表現により、REST アーキテクチャーの原則への完全準拠を実現します。

JEST の手法は、同様の目的を持つ JAX-RS などの他の手法とは対照的に、永続リソースに対して働きかけることはありません。JEST は完全にメタデータ駆動型であるため、その永続ドメイン・モデルの詳細を事前に知ることなく、あらゆる永続ドメイン・モデルに適用できる汎用の機能となります。

JEST を拡張して、OpenJPA の内部ランタイム状態 (クエリーの実行統計や、第 2 層キャッシュまたはキャッシュされたエンティティーのヒット率など) を表現することも可能です。このような内部情報にアクセスできれば、Web ブラウザー・ベースの監視コンソールを作成する上で役立ちます。

JEST で未解決の問題は、粒度の細かいデータ・セキュリティーです。リモート・クライアントがリソースとして永続データにアクセスするように設定すると、データの機密性およびセキュリティーの問題が持ち上がってきます。このプロジェクトでは、リクエスト側のクライアントの認証情報を基に、JEST レスポンスのコンテンツをきめの細かいデータ・レベルで認証または制御する方法を調査しているところです。JEST 実行環境が永続コンテキストおよび実行可能なクエリーの式ツリーを認識するとすれば、クライアントのロールに基づく一連のアクセス規則により、クエリー式ツリーのノードを断定するという方法が実現可能になります (現行の JESTServletは、デプロイメント時に JavaScript クライアントの基本 URL を書き換えることによって、クロスブラウザー・スクリプトを作成する必要をなくしています)。


参考文献

学ぶために

  • JEST: 以下のページをはじめ、JEST の Web サイトを調べてください。
    • JEST Representation: JEST で表現された XML または JSON フォーマットの永続 Java オブジェクト・グラフを見てください。
    • JEST Usage: JESTServlet の主デプロイメント・モードおよび補助デプロイメント・モードについて読んでください。

  • OpenJPA Documentation: OpenJPA および JEST の公式マニュアルを参照してください。

  • REST: ウィキペディアの REST に関する記事を読んでください。

  • Architectural Styles and the Design of Network-based Software Architectures」(Roy Thomas Fielding 著、2000年): REST の概念および用語を生んだ、Fielding の博士号論文です。

  • Dynamic, typesafe queries in JPA 2.0」(Pinaki Poddar 著、developerWorks、2009年9月): JPA 2.0 の Metamodel API に関する記事を読んでください。

  • JSR 317: Java Persistence 2.0: 現行の JPA 仕様を参照してください。

  • REST入門」(Stefan Tilkov 著、InfoQ、2007年12月): REST の概念を紹介しています。

  • Uniform Resource Identifiers (URI): Generic Syntax: この RFC (Request for Comments) で、標準 URI セグメントを定義しています。

  • Dojo および Gson: この 2 つの フレームワークの詳細を学んでください。

  • JSON: JSON ではオブジェクト参照をサポートしていないことについて説明しています。

  • 推移閉包: 永続クロージャーの背後にある概念について調べてください。

  • Fetch Groups: OpenJPA の FetchPlan インターフェースについて読んでください。

0 件のコメント:

コメントを投稿