Google App EngineのDatastoreをMapでラップする

Google App Engine for Javaで、ちょっとしたデータを永続化するためだけにDatastoreを触るのは面倒です。そこで、Datastoreをjava.util.Mapでラッピングしたクラスを作ってみました。(もうすでに誰かが作ってそうなにおいがぷんぷんするんですが軽く調べてみると見つからなかったので。)

他にもMemcacheのラッパークラス等も作りました。下記が作ったもの一覧です。

  • DatastoreMap: Datastoreをjava.util.Mapでラッピングしたクラス。
  • MemcacheMap: Memcacheをjava.util.Mapでラッピングしたクラス。
  • CachedDatastoreMap: オブジェクトを透過的にMemcacheにキャッシュするDatastoreMap。

App Engineにあまり詳しいわけではないので、おかしなところがあればご指摘いただければ幸いです。

サンプルコード

百聞は一見にしかず、ということでまずはサンプルコードです。下記のようにまるでHashMapでも触っているかのような感じでDatastoreを操作するプログラムを書くことができます。

// 「名前」に対応した「年齢」を格納する"people"というkindのMapオブジェクトを生成。
Map<String, Integer> map = new DatastoreMap<String, Integer>("people");

// Mapにデータ(「名前」と「年齢」)を格納。
map.put("Tom", 27);
map.put("Mary", 21);
map.put("John", 23);

// 「年齢」を取得。
int ageOfMary = map.get("Mary");

// 「名前」が”Tom"のデータを削除。
map.remove("Tom");

用途

BigTableがKey-ValueストアでRDBMSより機能が少ないとはいえ、さすがにMapよりは多くのことができます。当然のことながら、DatastoreMapを通してDatastoreにアクセスすると様々な機能が使えなくなります。

上記を踏まえて、次のような用途が考えられます。

  • LLAPIやSlim3、JDO*1などで複雑なデータを扱っているが、ちょっとしたデータを永続化したいとき。
  • インデックスやレンジスキャンが必要ないような簡単なアプリケーションを作成するとき。
  • App Engineを触ったことのない人が、とりあえず動かしてみたいとき(慣れ親しんだMapのようにストレージを操作できる)*2
  • LLAPIを勉強したいときの資料として(Mapならどんな機能があるか分かるので、実装がどうなっているのか覗きやすいのでは)。

ダウンロードと使い方

JARファイルとソースを下記からダウンロードできます。Apache License 2.0にて公開します。

GitHub - koher/Koherent-App-Engine-Library-for-Java: Koherent App Engine Library is a library for Google App Engine for Java. It includes "DatastoreMap" which enable to operate Datastores like java.util.HashMap, "DatastoreOutputStream" which enable to write data to Datastores like java.util.FileOutputStream and so on.
(画面右上あたりのDownload Sourcesをクリック→ZipかTarの形式を選択)

ダウンロードした圧縮ファイルを解凍し、中にあるJARファイルを/WEB-INF/libに放り込んでパスを通せばOKです(Eclipseなら、プロジェクトを右クリック→Properties→Java Build Path→Libraries→Add JARs→/WEB/INF/libの中に入れたJARファイルを選択)。

できること・できないこと

DatastoreMapとCachedDatastoreMapは、Mapに出来ることは一通りできます。entrySet()メソッドで取得したEntryオブジェクトを通してDatastoreを更新する等も可能です。ただし、keySet()メソッドとentrySet()メソッドを利用するためにはコンストラクタにキーのParserを渡してやる必要があります(詳細は下記の「Datastoreへの格納の仕組みとキーとParser」)。また、DatastoreMapとCachedDatastoreMapはアトミックな更新を実現するためのupdateメソッドを持つUpdatableMapインタフェースを実装しています。

MemcacheMapに関しては、Memcacheの機能上の問題でいくつか実装できなかったメソッドがあります。それらを呼び出すとUnsupportedOperationExceptionがスローされます。

できること

  • java.util.MapでDatastoreおよびMemcacheを操作(MemcacheMapは一部メソッドを未サポート)
  • キー、値ともにnull値の使用
  • 自作クラスのインスタンスをキーにしてデータを格納(ただし、toString()メソッドを実装する必要あり(下記))
  • 自作クラスのインスタンスを値にしてデータを格納(ただし、Serializableインタフェースを実装する必要あり)

DatastoreMap、CachedDatastoreMapのみ

  • put、remove等のメソッドを利用して更新、削除とデータの取得をアトミックに実行
  • updateメソッドを利用した既存データに対するアトミックな更新
  • keySet()、entrySet()等のメソッドで取得したSetやEntryを介したDatastoreの更新(ただし、インスタンス生成時にキーのParserを渡す必要あり)
  • put、remove等のメソッドでデータ取得→更新、削除の同時実行でConcurrentModificationExceptionが発生した際のリトライ回数の指定

CachedDatastoreMapのみ

  • 透過的にMemcacheにキャッシュを行いながらDatastoreを操作

できないこと

  • インデックスを利用したレンジスキャン
  • キー以外のプロパティによる検索
  • Limit、Offset、ソート順等の指定

MemcacheMapのみ

  • アトミックなインクリメント(値をLongにした場合しかサポートできないため)
  • containsValue、entrySet、keySet、valuesの各メソッド(Memcacheの機能上実装できなかったため)

アトミックな更新

DatastoreMapとCachedDatastoreMapでは、updateメソッドを用いてアトミックな更新が可能です。

// ユーザー別のカウンター
Map<String, Integer> counters = new DatastoreMap("counters");

// アトミックな更新
map.update(userId, new Updater<Integer>() {
	@Override
	public String update(Integer object) {
        // カウンターの値を1増加
		return object++;
	}
});

Datastoreへの格納の仕組みとキーとParser

Datastoreに格納する際には、キーとなるインスタンスのtoString()メソッドを利用してStringオブジェクトを取得し、Datastoreのキーとしています。このため、自作クラスのインスタンスをキーとしてデータを格納するためにはtoString()メソッドがインスタンスにとって一意な値を返すようにオーバーライドする必要があります。具体的には、toString()メソッドは、equalsメソッドがtrueを返す二つのオブジェクトが同じ値を、falseを返すオブジェクト間では違う値を返すようにする必要があります

keySet()およびentrySet()メソッドを利用するには、String化されたキーを復元する必要があります。このため、これらのメソッドを利用するにはStringをパースしてキーを作成するParserをmapに渡してやる必要があります。プリミティブに対するParser(IntegerParser、LongParser等)とStringParserはライブラリに含まれています(これらのクラスはSingletonパターンを採用しており、コンストラクタでnewするのではなく、StringParser.getInstance()でインスタンスを取得します)。

キーがnullの場合はそのままではDatastoreに格納できないため、他のデータと衝突しないようにLong 1Lをキーとして格納します。

課題

時間がないので後で書きます。

雑感

一昨日に「DatastoreをMapのように操作したいなぁ」と思い軽く探してみても見つからなかったので勢いで作ったのですが、LLAPIのいい勉強になりました。まだまだ理解が浅いのでおかしなところがあれば容赦なくつっこんでいただけるとありがたいです。

簡単なテストはしましたが、まだ十分にテストできてないです。色々バグがあるかも。Javadocもまともに書けてませんが、所詮Mapなんでとりあえずいいかと(ry

これから出かけてそのまま夜行バスで東京に帰ります。

追記(2010.03.07)

より高速なFastDatastoreMap等も実装したのでご利用下さい。putやremoveの戻り値を省略することで高速化しています。

*1:App EngineのJDOはいけてないみたいなので、LLAPIを直接叩くか、Slim3などのラッパーを利用するのがいいでしょう。

*2:初めてApp Engineに触れる人がどうやってこのライブラリを見つけるのか疑問ですが。