2011年3月6日日曜日

Objectify-Appengine による Twitter のマイニング

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 と言えます。

「ORM ライク」とは?

オブジェクト・リレーショナル・マッピングは、オブジェクト指向のデータ・モデルとリレーショナル・データベースの間のいわゆるインピーダンス・ミスマッチを克服するのに最もよく使われている手段です (「参考文献」を参照)。非リレーショナルの世界には、インピーダンス・ミスマッチはありません。したがって、Objectify は実際には ORM ライブラリーではなく、むしろ ONRM (オブジェクト非リレーショナル・マッピング) ライブラリーに近いのですが、あまりにも多くの頭辞語にうんざりしている人にとっては、「ORM ライク」と省略するほうが、おあつらえ向きです。

Objectify が Hibernate と似ている点は、Bigtable に対して POJO をマッピングして利用できるようにすることで、これは GAE における抽象化と見なすことができます。Objectify では JPA アノテーションのサブセットに加え、GAE データストア固有の機能に対処するために、独自のアノテーションを採用しています。また、Objectify ではデータの関係を使用することが可能で、GAE のフィルタリングおよびソートの概念をサポートするクエリー・インターフェースを公開します。

以降のセクションでは、Objectify でのマッピングとデータ・パーシスタンスを実際に試してみるために、Google の Bigtable を使用してアプリケーション・データを保管するサンプル・アプリケーションを開発します。記事の後半では、Bigtable に保管されたデータを GAE Web アプリケーションで使用します。

Bigtable の概要

今回は、「レースとランナー」ドメインの例は使用せず、駐車違反切符の例も同じく省略します。代わりに用いる例は、Twitter のマイニングです (Twitter のマイニングは MongoDB について紹介した先月の記事を読んだ読者にとっては、お馴染みのアプリケーション・ドメインです)。今回の記事では Twitter で (私や読者の皆さんに) リツイートを返したユーザーだけでなく、リツイートを返したユーザーのなかで、誰が最も影響力を持っているかについても調べることにします。

このアプリケーションでは、Retweet  User という 2 つのドメイン・クラスを作成する必要があります。Retweet オブジェクトは当然、Twitter アカウントから送信されたリツイートを表します。User オブジェクトが表すのは、マイニング対象とするアカウント・データを持つ Twitter ユーザーです (この User オブジェクトは、GAE の User オブジェクトとは別のものであることに注意してください)。すべての Retweet は、User との間に関係があります。

Bigtable について

Bigtable は、Google App Engine からアクセスできる、列指向の NoSQL データストアです。Bigtable は基本的に、大々的に分散されたパーシスタンス・マップであり、リレーショナル・データベースに見られるようなスキーマはなく、データ値のキーと属性に関するクエリーを行うことができます。GAE と Bigtable との関係は、Amazon Web Services と SimpleDB との関係とよく似ています。

Objectify は Google の下位レベルの Entity API を使用して、直観的にドメイン・オブジェクトを GAE データストアにマッピングします。EntityAPI については以前の記事 (「参考文献」を参照) で紹介したので今回は詳しく説明しませんが、知っていなければならない重要な点として、Entity API ドメインの名前は kind (種類) タイプになります。つまり、User は論理的に User kind にマッピングされるということです。kind は、リレーショナルで言うテーブルと似ています (それよりもさらに近い例として、kind はキーと値を保持するマップのようなものだと考えてください)。ドメインの属性は基本的にリレーショナルで言う列名であり、属性値は列の値です。Amazon の SimpleDB とは異なり、GAE データストアは blob (「参考文献」を参照) を含む豊富なデータ型、そしてあらゆる種類の数値、日付、リストをサポートします。

Objectify でのクラス定義

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 ドメイン・クラスにちょっとした振る舞いを追加するために、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 インスタンス側で RetweetKey を永続化する必要はありません。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 のインスタンスに追加されると同時に、正式に互いに関連付けられることになります。

Twitter のマイニング

次のステップは、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 件のコメント:

コメントを投稿