技術ネタ‎ > ‎

Datastoreのlow-level API(低レベルAPI)

個人的な思いとしては、GAE/JのDatastoreについて、JDOから入ると間違った理解をしやすい/ハマリやすいと思ってるんで、Low-level APIから入って、それからJDOを使っていくかLow-level APIで行くか、を選択するのが良いと思ってます。GAE/JのJDOを使う時は、まずはlow-level APIから入って、それから「JDOだとこんな事を便利にやってくれるんだ」とプラスαの部分を積んでいく方が良い。JDOから入るとどーしてもRDBのORMだという認識が頭から抜けずにはまる人が多いよぅに思うんですよね。スタンスとしてはJDOを使う事に否定的なわけではなく、学習の順序という話です。

Entity

low-level APIを使うと、JDOのようなPOJOにアノテーション、タイプセーフなpropertyにアクセサ…と言った物は無いです。全てのエンティティをEntityクラスとして扱う必要があります。

コンストラクタ

  • 一番単純なコンストラクタ
    Entity entity = new Entity(String kind)
    Kind名が必須です。主Keyを指定しない事になるので、永続するタイミングで主Keyは自動生成されます。
    JDOでいう@PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key;と同じ状態です。
  • 主Keyを指定するコンストラクタ
    Entity entity = new Entity(String kind, String keyName)
    keyNameはKeyを作成する時に使用する文字列のnameのアレです。Keyオブジェクトを渡す方がわかりやすいと思うんだけど、うっかりそれをやるとそのKeyを持つEntityを親としたEntityGroupを形成してしまいます(次に説明するAncectorKeyでのコンストラクタが適用されてしまう)。
  • 親キーを指定するコンストラクタ
    Entity entity = new Entity(String kind, Key ancestorKey)
    low-level APIでEntityGroup(用は単なる親子関係ですね)を形成したい場合はこれを使います。第二引数で指定したKeyに対応するエンティティの子エンティティとなるエンティティが作成されます。EntityGroupといっても、結局「エンティティのKeyが親を持つか?持たないのか?」という事でしか無いのです。
    JDOだと@Persistent @Extension(vendorName = "datanucleus", key = "gae.parent-pk", value = "true") private Key ancestor;という方法で子エンティティに親エンティティの主キーをバインドしたり、設定したりしてEntityGroupを形成するRelationshipの組み方がありますが、これが近いかもしれません。
  • 主キーと親キーを指定するコンストラクタ
    Entity entity = new Entity(String kind, String keyName, Key ancestorKey)
  • 主キーを指定するコンストラクタ[SDK1.2.5]
    Entity entity = new Entity(Key primaryKey)
    SDK1.2.5で追加されました。主キーを指定してEntityを作成します。Keyオブジェクトには親エンティティのKeyを含める事ができるので、EntityGroupの子供になる場合もこのコンストラクタが使えます。

属性値の設定

  • void Entity#setProperty(String propertyName, Object propertyValue)
    属性の名前を指定して、エンティティに値を格納します。ちなみに、BlobやText等のインデックス対象外の属性の場合はsetUnindexedProperty()を使う必要があります。JDOのアクセサ経由の属性の設定と違い、属性名が文字列型なあたりがtype safeではありませんし、与える値もObject型なあたりがtype safeではありません。

属性値の取得

  • Object Entity#getProperty(String propertyName)
    存在しない属姓名が指定されてもExceptionを投げずにnullを返してきます。こちらもJDOと違い、type safeではないのでJavaで使う場合はcastがウザイです。
    指定された属性名の属性を持っているか?を確認する為にboolean hasProperty(String propertyName)っていうメソッドもあります。指定された属性名に対応する属性が存在しない場合にfalseを返します。指定された属姓名に対応する値がnullの場合でも、属性さえあればtrueを返しますので、属性が存在しない/存在するけどnull、の区別をつける事が出来ます。
    また、JDOを使って書き込みを行っている場合は、エンティティのバージョン管理という意味の楽観的排他制御やListPropertyのインデックス等の制御をする為にJDOが独自に付加した属性が存在していたりします。
    気をつけなければいけない点としてIntegerで書き込んた属性値も実際にはLongに変換されて書き込まれるという点です。当然、取得するとLong型で取得できてしまいます。
  • 特殊な属性値の取得
    Kind名を取得するにはString getKind()、主キーを取得するにはKey getKey()、親キーを取得するにはKey getParent()を使います。
  • 主キーを意味する属性名
    主キーは特殊な扱いで、KEY_RESERVED_PROPERTYという定数で主キーを意味する属姓名が定義されています。値は"__key__"で、GAE/Pythonを使ってる人にはおなじみの値です。

Query

コンストラクタ

  • Kindを指定するコンストラクタ
    Query query = new Query(String kind)
  • Kindと親キーを指定するコンストラクタ
    Query query = new Query(String kind, Key ancestorKey)
  • 親キーのみ指定するコンストラクタ
    Query query = new Query(Key ancestorKey)

コンストラクタに親キーのみ指定するものもあるんですが、それを使って何かが取得できた記憶が無いので使い方がよくわかってません。コレの使いどころがわかっている方、教えて下さると嬉しいです。 →ローカルでは動作しませんが、Production環境では「親キーに属する全てのエンティティを、Kindを横断してごっそり取得する」という動作をします。

フィルタ

  • Query addFilter(String propertyName, Query.FilterOperator operator, java.lang.Object value)
    見たまんまです。Query.FilterOperatorにはEQUAL, GREATER_THAN[_OR_EQUAL], LESS_THAN[_OR_EQUAL]という定数があります。

ソート

  • Query addSort(String propertyName[, Query.SortDirection direction])
    これも見たまんまです。Query.SortDirectionも想像の通りASCENDING, DESCENDINGが定数として定義されています。

キーのみのクエリ

  • Query setKeysOnly()
    重要。キーのみのスキャンを行う時はコレを使います。もちろん、取得できるのはKeyのみで、属性にはアクセスできないEntityが返ってきます。
    JDOでいうsetResult("key")とほぼ同じです。

DatastoreService

Datastoreへアクセスする為のサービスクラスで、大概の処理はこのDatastoreServiceが担当するんですが案外シンプルです。そんなに機能が無いからですw。他のサービスクラスと同様に、サービスクラス用のファクトリからインスタンスを取得します。
  • DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();

保存する

めちゃ簡単です。基本的には Key DatastoreService#put(Entity entity)または List<Key> DatastoreService#put(Iterable<Entity> entities)の2種類のメソッドです。後述しますが、Transaction中で実行したい場合は第一引数にTransactionを与えます。

削除する

これも簡単。void DatastoreService#delete(Keys... keys)が基本形。他に、Iterable<Key>を引数にする事も出来ます。保存と同様に、Transaction中で実行したい場合は第一引数にTransactionを与えます。

Keyを指定して取得する

  • Entity DatastoreService#get(Key key)
主キーを指定してEntityを取得します。個人的にはあんまり使った記憶がありませんが、Map<Key, Entity> DatastoreService#get(Iterable<Key> keys)っていうメソッドもあります。
親キーを指定した取得に専用メソッドは無く、後述するQueryクラスを使用する事になります。

Queryオブジェクトでクエリする

上の項目で説明したQueryを引数にしてDatastoreService#prepare(Query query)を実行する事でPreparedQueryが取得できるのですが、ここから「何を/どのように取得するか?」といったカンジになります。
重要な点として、1000件以上の結果は返せないという点です。とはいえ1000件以上取得できるJDOも、1000件以上を扱おうとすると遅くて実用的ではない事も多いので、何が何でも1リクエストに対して1000件以上を一気に!…って時は色々工夫が必要になります。工夫した結果、low-level APIにたどり着くのかもしれません。

結果セットの取得メソッド

以下のパターンがあります。一番良く使うのはasList()かな?
  • List<Entity> asList(FetchOptions fetchOptions)
  • Iterable<Entity> asIterable([FetchOptions fetchOptions])
  • Iterator<Entity> asIterator([FetchOptions fetchOptions])

FetchOptions

個人的にはasList()メソッドで必要だから常にoffset(0)を使っている程度で、あまり使いこなせていないので細かい説明ができません、ゴメンナサイ。でも大体名前の通りなんでしょう。
  • FetchOptions offset(int offset)
  • FetchOptions limit(int offset)
  • FetchOptions chunkSize(int offset)
  • FetchOptions prefetchSize(int offset)

特殊なクエリ

  • Entity asSingleEntity()
    名前の通り、一件だけ取得します。一件も無い時はnullが返されますが、フィルタの結果に2件以上存在した場合は PreparedQuery.TooManyResultsExceptionが投げられます。
  • int countEntities()
    名前のとおり、結果セットの件数を返します。JDOでいうsetResult("count(this)")と同じです。
ちなみに、sdk1.2.2での現象で不具合なのか仕様なのかよくわかりませんが、例えば"Kind"というKind内でkind1(ルート), kind2(kind1の子エンティティ)という、同じKind内のEntity同士でEntityGroupを構築した場合、Query("kind", kind1.getKey())でクエリするとkind1, kind2の2件が返されます。なんのこっちゃわかりません(kind2だけが返される事を期待するよね?)。

自動採番のキーを作成する[SDK1.2.5]

  • allocateIds(String kind, long num)
    Kindと、作成するキーの個数を指定してKeyを取得します。
  • allocateIds(Key parent, String kind, long num)
    親キーを指定してEntityGroupを構成するKeyを作成したいときに使います。
自動採番されるKeyは1.2.2まではPUTしなければ取得できませんでしたが、1.2.5からいつでもIDが割り当てられたKeyを取得する事ができます。Transactionの項にサンプルを追加しておきました。

Transaction

JDOと似たような使い方が出来ます。

Entity parent = new Entity("kind");
parent.setProperty("property1", "hoge");

DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
Transaction transaction = datastoreService.beginTransaction();
try {
  Key parentKey = datastoreService.put(transaction, parent);
  List<Entity> children = new ArrayList<Entity>();
  Entity child1 = new Entity("child", parentKey);
  child1.setProperty("property1", "child1");
  children.add(child1);
  Entity child2 = new Entity("child", parentKey);
  child2.setProperty("property1", "child2");
  children.add(child2);
  datastoreService.put(transaction, children);
  transaction.commit();
} finally {
  if (transaction.isActive()) {
    transaction.rollback();
  }
}

SDK1.2.5のallocateIds()を使った場合

DatastoreService service = DatastoreServiceFactory.getDatastoreService();
KeyRange parentKeys = service.allocateIds("Parent", 1);
Key parentKey = parentKeys.getStart();
KeyRange childKeys = service.allocateIds(parentKey, "Child", 2);
Iterator<Key> childKeysIterator = childKeys.iterator();
Entity parent = new Entity(parentKey);
parent.setProperty("property1", "parent");
Entity child1 = new Entity(childKeysIterator.next());
child1.setProperty("property1", "child-1");
Entity child2 = new Entity(childKeysIterator.next());
child2.setProperty("property1", "child-2");

Transaction transaction = service.beginTransaction();
try {
  service.put(transaction, Arrays.asList(parent, child1, child2));
  transaction.commit();
} finally {
  if (transaction.isActive()) {
    transaction.rollback();
  }
}

Comments