NoSQL データストアが発端となって、この数年間の Java™ 世界では急激な革新が進んでいます。さらにデータストア自体 (CouchDB、MongoDB、Bigtable など) だけでなく、その有用性を拡張するためのツールも登場してきています。
一連のツールのリーダー格となっているのは、NoSQL の致命的な問題の 1 つに対処する ORM ライクなマッピング・ライブラリーです。これらのマッピング・ライブラリーは、Hibernate がリレーショナル・データストアに対して行っているのと同じような方法で、スキーマレス・データストアの一般的なオブジェクトである POJO (Plain Old Java Object) をいかに効率的にマッピングして、POJO を役に立たせるかという問題に対処します。
ライブラリーは、JPA アノテーションを付けたオブジェクトが Amazon のSimpleDB とほとんどシームレスに連動できるようにします。SimpleJPA については何回か前の記事で紹介しましたが、そのなかで、このライブラリーは JPA をベースとしているにも関わらず、JPA 仕様のすべてを実装しているわけではないことを指摘しました。これは、JPA ではリレーショナル・データベースを使用することが意図されている一方で、SimpleDB (ひいては、その小さなヘルパー・ライブラリーである SimpleJPA) はリレーショナル・データベースを避けているからです。一方、他のプロジェクトでは最初から完全な JPA 仕様に従おうとすらせずに、単に必要なところだけを拝借しています。そのようなプロジェクトの 1 つが、今月の記事で話題にする Objectify-Appengine です。
Objectify-Appengine: オブジェクト非リレーショナル・マッピング・ライブラリー
Objectify-Appengine (以降、Objectify とします) は、Bigtable でのデータ・パーシスタンス、ひいては GAE でのデータ・パーシスタンスも単純化する ORM ライクなライブラリーです。マッピング層としての Objectify は、簡潔な API を介して POJO と Google のデータストアの間に入り込みます。データを Java オブジェクトという形で永続化および取得するために、JPA アノテーションのお馴染みのサブセット (ただし、Objectify は JPA 仕様のすべてを実装してはいません) と併せて、少数のライフサイクル・アノテーションを使用します。本質的に、Objectify は Google の Bigtable 専用に設計された、さらに軽量の Hibernate と言えます。
Objectify が Hibernate と似ている点は、Bigtable に対して POJO をマッピングして利用できるようにすることで、これは GAE における抽象化と見なすことができます。Objectify では JPA アノテーションのサブセットに加え、GAE データストア固有の機能に対処するために、独自のアノテーションを採用しています。また、Objectify ではデータの関係を使用することが可能で、GAE のフィルタリングおよびソートの概念をサポートするクエリー・インターフェースを公開します。
以降のセクションでは、Objectify でのマッピングとデータ・パーシスタンスを実際に試してみるために、Google の Bigtable を使用してアプリケーション・データを保管するサンプル・アプリケーションを開発します。記事の後半では、Bigtable に保管されたデータを GAE Web アプリケーションで使用します。
今回は、「レースとランナー」ドメインの例は使用せず、駐車違反切符の例も同じく省略します。代わりに用いる例は、Twitter のマイニングです (Twitter のマイニングは MongoDB について紹介した先月の記事を読んだ読者にとっては、お馴染みのアプリケーション・ドメインです)。今回の記事では Twitter で (私や読者の皆さんに) リツイートを返したユーザーだけでなく、リツイートを返したユーザーのなかで、誰が最も影響力を持っているかについても調べることにします。
このアプリケーションでは、Retweet
と User
という 2 つのドメイン・クラスを作成する必要があります。Retweet
オブジェクトは当然、Twitter アカウントから送信されたリツイートを表します。User
オブジェクトが表すのは、マイニング対象とするアカウント・データを持つ Twitter ユーザーです (この User
オブジェクトは、GAE の User
オブジェクトとは別のものであることに注意してください)。すべての Retweet
は、User
との間に関係があります。
Objectify は Google の下位レベルの Entity
API を使用して、直観的にドメイン・オブジェクトを GAE データストアにマッピングします。Entity
API については以前の記事 (「参考文献」を参照) で紹介したので今回は詳しく説明しませんが、知っていなければならない重要な点として、Entity
API ドメインの名前は kind (種類) タイプになります。つまり、User
は論理的に User kind にマッピングされるということです。kind は、リレーショナルで言うテーブルと似ています (それよりもさらに近い例として、kind はキーと値を保持するマップのようなものだと考えてください)。ドメインの属性は基本的にリレーショナルで言う列名であり、属性値は列の値です。Amazon の SimpleDB とは異なり、GAE データストアは blob (「参考文献」を参照) を含む豊富なデータ型、そしてあらゆる種類の数値、日付、リストをサポートします。
User
オブジェクトはかなり基本的なもので、ここに定義するのは名前、そして Twitter の OAuth 実装に関連する 2 つの属性だけです。OAuth 実装を使用する理由は、その直観的な権限付与の手法にあります。OAuth のパラダイムでは、ユーザーのパスワードを保管するのではなく、ユーザーが、そのユーザーに許可されるアクセス権限を表すトークンを保管します。OAuth は、通貨ではなくアクセス権限データを使用するという点を抜かせば、クレジット・カードのような働きをします。つまり、あらゆる Web サイトにユーザー名とパスワードを渡す代わりに、ユーザーがサイトに対し、ユーザー名とパスワードに関する情報にアクセスする許可を与えるのです (OAuth は OpenID と似ていますが、同じではありません。詳細については「参考文献」を参照してください)。
リスト 1. User オブジェクトの先頭部分
import javax.persistence.Id; public class User { @Id private String name; private String token; private String tokenSecret; public User() { super(); } public User(String name, String token, String tokenSecret) { super(); this.name = name; this.token = token; this.tokenSecret = tokenSecret; } public String getName() { return name; } //... } |
リスト 1 を見ると、User
クラスに関連付けられているパーシスタンス固有のコードは唯一、@Id
アノテーションしかありません。import
からわかるように、@Id
は標準 JDO です。GAE データストアでは、ID またはキーを String
あるいは Long/long
のどちらにでもすることができます。リスト 1 では、Twitter アカウントの名前をキーとして指定しています。また、3 つすべてのプロパティーを引数に取るコンストラクターも作成して、新しいインスタンスを簡単に作成できるようにしています。注目する点として、このオブジェクトを Objectify で使用するためにゲッターとセッターを実際に定義する必要はありません (ただし、プログラムでプロパティーにアクセスしたり、プロパティーを設定したりする場合には必要となります)。
User
オブジェクトは、データストアに永続化されると User kind になります。このエンティティーが持つのは、name
というキーと、token
および tokenSecret
という 2 つのプロパティーで、そのすべてが String
です。かなり簡単な話だと思いませんか?
次は、User
ドメイン・クラスにちょっとした振る舞いを追加するために、User
オブジェクトがその名前で自己を検出できるようにするクラス・メソッドを作成します。
リスト 2. 名前で User を検出する
//inside User.java... private static Objectify getService() { return ObjectifyService.begin(); } public static User findByName(String name){ Objectify service = getService(); return service.get(User.class, name); } |
新しくなったリスト 2 の User
では、いくつかのことが行われています。Objectify
を使用するには、もちろん Objectify
を起動する必要があります。そこで、Objectify
のインスタンスを取得します。このインスタンスが、CRUD のような操作をすべて処理します。Objectify
クラスは、大まかに言って Hibernate の SessionFactory
クラスのようなものだと考えることができます。
Objectify
には単純な API があります。個々のエンティティーをそのキーで見つけるには、クラスのタイプとキーを引数に取る get
メソッドを呼び出せばよいのです。したがってリスト 2 では、User
クラスと対象の名前を指定して get
を呼び出しています。Objectify の例外は非チェック例外であることにも注目してください。これは、一連の Exception
型をキャッチすることについては心配する必要がないことを意味します。例外が発生しないと言っているわけではありません。ただ単に、コンパイル時に例外に対処する必要がないということです。例えば、User kind が見つからなければ、get
メソッドは NotFoundException
をスローします (Objectify には、代わりに null
を返す find
メソッドもあります)。
次は、インスタンスの振る舞いに取り掛かります。User
インスタンスには、すべてのリツイートを影響順にリストアップする機能をサポートさせなければなりません。したがって別のメソッドを追加する必要がありますが、その前に、まずは Retweet
オブジェクトをモデル化しておきます。
ご想像のとおり、Retweet
は Twitter のリツイートを表します。このオブジェクトが保持することになる属性は多数あり、リツイートを所有する User
オブジェクトに対する関係もその 1 つです。
前に述べたように、GAE データストア内での ID またはキーは、String
または Long
/long
のいずれかでなければなりません。従来のデータベースの場合と同じく、GAE データストア内のキーは一意に決まるものであるため、User
オブジェクトのキーには、本来一意である Twitter アカウントの名前が使われているというわけです。リスト 3 に記載する Retweet
オブジェクトのキーは、tweet id
とそのリツイートを返したユーザーの組み合わせとなります (Twitter では同じテキストのツイートを 2 回投稿することは許可されていないため、今のところ、このキーは理にかなっています)。
リスト 3. Retweet を定義する
import javax.persistence.Id; import com.googlecode.objectify.Key; public class Retweet { @Id private String id; private String userName; private Long tweetId; private Date date; private String tweet; private Long influence; private Key<User> owner; public Retweet() { super(); } public Retweet(String userName, Long tweetId, Date date, String tweet, Long influence) { super(); this.id = tweetId.toString() + userName; this.userName = userName; this.tweetId = tweetId; this.date = date; this.tweet = tweet; this.influence = influence; } public void setOwner(User owner) { this.owner = new Key<User>(User.class, owner.getName()); } //... } |
リスト 3 のキー id
は String
であり、tweetId
と username
を結合したものになります。リスト 3 に記載されている setOwner
メソッドの意味は、関係についての説明を読めば理解できるはずです。
このアプリケーションでの Retweet
と User
との間には関係があります。つまり、すべての User
は Retweet
の論理コレクションを保持し、すべての Retweet
はその User
に直接結び付いたリンクを保持します。リスト 3 をもう一度見てください。通常とは異なり、Retweet
オブジェクトには User
型の Key
オブジェクトがあります。
Objectify がオブジェクト参照ではなく Key
を使用するのは、GAE が従来とは異なるデータストアであり、何よりもまず、参照整合性がないという点を反映してのことです。
この 2 つのオブジェクト間の関係に必要なのは、実際のところ、Retweet
オブジェクトでの確実な関連付けだけです。そうした理由から、Retweet
のインスタンスは User インスタンスに対する直接の Key を保持します。したがって、User
インスタンス側で Retweet
Key
を永続化する必要はありません。User
インスタンスは単純にリツイートに対し、そのインスタンス自体にリンクしている Key
をクエリーで要求することができるからです。
それでもなお、オブジェクト間の対話をより直観的なものにするために、リスト 4 では Retweet
を引数に取るいくつかのメソッドをUser
に追加しました。これらのメソッドによって 2 つのオブジェクト間の関係は強固になり、User
が Retweet
の所有権を直接設定できるようになります。
リスト 4. Retweet を User に追加する
public void addRetweet(Retweet retweet){ retweet.setOwner(this); Objectify service = getService(); service.put(retweet); } public void addRetweets(List<Retweet> retweets){ for(Retweet retweet: retweets){ retweet.setOwner(this); } Objectify service = getService(); service.put(retweets); } |
リスト 4 では、User
ドメイン・オブジェクトに 2 つの新しいメソッドを追加しました。一方のメソッドは Retweet
のコレクションを処理し、もう一方は 1 つのインスタンスでのみ動作します。リスト 2 で定義した service
への参照があること、そしてその put
メソッドは単一のインスタンスと List
の両方を処理するようにオーバーロードされていることに注目してください。この例での関係は、所有側のオブジェクトによっても処理されます。具体的には、User
インスタンスがそれ自体を Retweet
に追加するということです。したがって、Retweet
はそれぞれ個別に作成されますが、User
のインスタンスに追加されると同時に、正式に互いに関連付けられることになります。
次のステップは、User
オブジェクトに finder のようなメソッドを追加することです。このメソッドによって、オブジェクトが所有するすべての Retweet
をその影響順、つまり所有しているアカウントを筆頭に、そのアカントに対してリツイートを返したアカウントに至るまでを、順にリストアップできるようにします。そして、フォロワーの最も多いアカウントから、フォロワーが最も少ないアカウントまでを追跡します。
リスト 5. 影響順のリツイート
public List<Retweet> listAllRetweetsByInfluence(){ Objectify service = getService(); return service.query(Retweet.class).filter("owner", this).order("-influence").list(); } |
リスト 5 のコードは User
オブジェクト内に含まれます。このコードが返すのは、influence
プロパティー (整数値) の順にリストアップした Retweet
の List
です。この例での「-
」は、Retweet
を降順 (最も高い値から低い値の順) でリストアップすることを意味します。Objectify のクエリー・コードに注目してください。service
インスタンスはプロパティー (この例では owner
) を基準としたフィルタリングをサポートするだけでなく、結果の順序付けもサポートします。さらに、非チェック例外の連続パターンにより、コードが大幅に簡潔になっていることも注目の点です。
GAE データストアは、実行されるすべてのクエリーに対して索引を使用します。エンティティーに含まれる単一のプロパティーには自動的に索引が付けられることから、高速読み取り操作に役立ちます。その一方、複数のプロパティーを指定してクエリーを実行することになった場合には (リスト 5 のように、owner
を指定してクエリーを実行してから influence
を指定してクエリーを実行するといった場合)、GAE に対して datastore-index.xml
ファイルを提供する必要があります。これにより、GAE に対して実行する予定のクエリーを事前に通知します。リスト 6 は、複数のプロパティーを指定してクエリーを実行できるようにするためのカスタム索引です。
リスト 6. GAE データストアのカスタム索引を定義する
<?xml version="1.0" encoding="utf-8"?> <datastore-indexes autoGenerate="true"> <datastore-index kind="Retweet" ancestor="false"> <property name="owner" direction="asc" /> <property name="influence" direction="desc" /> </datastore-index> </datastore-indexes> |
最後になりましたが、同じく重要な作業として、ドメイン・オブジェクトを永続化するための何らかの機能を追加しなければなりません。お気付きかもしれませんが、User
オブジェクトと Retweet
オブジェクトの関係には暗黙的なワークフローがあります。それは、User
インスタンスが作成 (そして GAE データストアに保存) されてからでないと、関連する Retweet
を論理的に追加することができないということです。
リスト 7 では、User
オブジェクトに save
メソッドを追加していますが、Retweet
オブジェクトにはこのメソッドを追加する必要はありません。Retweet
は User
インスタンスに追加されると自動的に保存されるからです。インスタンスに追加するための手段としては、addRetweet
メソッドと addRetweets
メソッドを使用します (リスト 4 の service.put
の呼び出しを見てください)。
リスト 7. User を保存する
public void save(){ Objectify service = getService(); service.put(this); } |
このコードがいかに簡潔かを見てください。これが、Objectify API を活用したコードです。
いよいよ Twitter マイニング・アプリケーションを用意します。それには、Servlets API を関連付ける作業が多少必要になります。Twitter にログインし、リツイート・データを抽出し、そして最後に粋なレポートを表示するためには、サーブレットを使用します。しかしこの作業については皆さんの想像にお任せするとして、ここでは Objectify を扱うための最後の要件に重点を絞ります。それは、ドメイン・クラスを手動で登録することです。
Objectify はドメイン・クラスを自動的にロードしません。つまり、クラス・パスをスキャンしてエンティティーを調べることはしないため、Objectify に対象となるクラスを事前に指示しておかなければ、これらのクラスに Objectify APIを介してアクセスして使用することはできません。ドメイン・クラスを登録するには、ObjectifyService
オブジェクトを使用します。これは当然、このオブジェクトの CRUD のような振る舞いを呼び出す前に行う必要があります。幸い、ここで作成しているのは GAE にデプロイする単純な Web アプリケーションなので、Servlet API を使用して、2 つのクラスを ServletContextListener
インスタンスに登録することができます。
ServletContextListener
には、コンテキストが作成されるときに呼び出されるメソッドと、コンテキストが破棄されるときに呼び出されるメソッドの 2 つがあります。コンテキストは Web アプリケーションを最初に起動したときに作成されるので、以下のコードは問題なく機能するはずです。
リスト 8. ドメイン・オブジェクトを登録する
import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import com.googlecode.objectify.ObjectifyService; public class ContextInitializer implements ServletContextListener { public void contextDestroyed(ServletContextEvent arg) {} public void contextInitialized(ServletContextEvent arg) { ObjectifyService.register(Retweet.class); ObjectifyService.register(User.class); } } |
リスト 8 に記載した ServletContextListener
の単純な実装では、2 つの Objectify ドメイン・クラス、User
および Retweet
を登録しています。ServletContextListener
インスタンスの登録先は、Servlet API に準拠して web.xml
ファイルとなります。サンプル・アプリケーションが Google のサーバーで起動されると、リスト 8 のコードが呼び出されます。それ以降、この 2 つのドメイン・オブジェクトを使用するサーブレットはすべて、苦労せずに何の問題もなく機能します。
以上で、2 つのクラスを作成し、その関係と CRUD のような機能を定義しました。この作業で一貫して使用したのは、Objectify-Appengine です。サンプル・アプリケーションを作成するなかで、Objectify API が持ついくつかの特徴に気付いたと思います。例えば、Objectify API は Java コードにありがちな冗長性を大幅に削減します。また、使用する標準 JPA アノテーションの数は限られているため、開発者は Hibernate のように JPA に対応するよう強化されたフレームワークをすんなりと使いこなせるようになります。全体的に見ると Objectify API は、GAE を対象としたドメインのモデル化を、より簡単かつ直観的な作業にすることから、結果的に開発者の生産性が向上することになります。
この記事に続く後半では、今回作成したドメイン・アプリケーションを次のレベルに引き上げ、OAuth、Twitter API (Twitter4J を使用)、そして Ajax と JSON を使って完成させます。そのための一連の手順は、少し複雑になるかもしれません。それは、このアプリケーションのデプロイ先が、実装に多少の制約を課す Google App Engine だからです。その一方、実にスケーラブルなクラウド・ベースの Web アプリケーションとして完成させることができるというプラスの面もあります。来月の記事では、サンプル・アプリケーションを GAE にデプロイするための準備に取り掛かる際に、この GAE にデプロイすることに伴うトレードオフについて詳しく探ります。
0 件のコメント:
コメントを投稿