追記
このエントリについては今後はWikiでメンテナンスしていきますので、最新の情報はWikiで確認して下さい。
追記
スティルハウス社の佐藤さんよりコメントを頂いたので一部の表現を修正しました。取り消し線+青字にしたりしてます。
追記
ひがさんよりコメントを頂いたので、最後のTransactionについてのソースコードのサンプルを一部修正しています。子Entityをput()する呼び出しの第一引数にTranscationを指定するように修正しました。
ここから
GAE/JのLow-level API(主にDatastore周り)については基本的にJavaDocしかなくて情報量が少ないと思ったので、それなりに使っていくための簡単な説明を書いてみる(@fumokmm氏の発言で思いつきました、Thx!)。
個人的な思いとしては、GAE/JのDatastoreについてJDOから入るのは間違いの元だ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)
- 属性値の設定
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を使ってる人にはおなじみの値です。
Keyの部分以外は単純に言うとMapみたいなモンですからシンプルです。属性へのアクセスがtype safeじゃない(全てのエンティティをひとつのクラスで扱うんだから当然だけど)点が特徴的です。low-level APIではJDOとは違いスキーマレスという特徴を活用する事ができるので、その場合にはhasProperty()
メソッドが大活躍するはず。
DatastoreService
Datastoreへアクセスする為のサービスクラスで、大概の処理はこのDatastoreService
が担当するんですが案外シンプルです。そんなに機能が無いからですw。他のサービスクラスと同様に、サービスクラス用のファクトリからインスタンスを取得します。DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
保存する
めちゃ簡単です。基本的には Key DatastoreService#put(Entity entity)
または List<Key> DatastoreService#put(Iterable<Entity> entities)
の2種類のメソッドです。
削除する
これも簡単。void DatastoreService#delete(Keys... keys)
が基本形。他に、Iterable<Key>
を引数にする事も出来ます。
Keyを指定して読み出す
Entity DatastoreService#get(Key key)
主キーを指定してEntity
を取得します。
個人的にはあんまり使った記憶がありませんが、Map<Key, Entity> DatastoreService#get(Iterable<Key> keys)
っていうメソッドもあります。
親キーを指定した取得に専用メソッドは無く、後述するQuery
クラスを使用する事になります。
クエリする(1)
実際の動作はスキャンなので、クエリという単語だと誤解を生みそうですが、クラス名やドキュメントでもクエリって書かれてるのでここでもクエリと書いておきます。Query
クラスでクエリを組み立てますが、JDOやJPAを使っている人はjavax.jdo.Query
やjavax.persistence.Query
とは別なので注意が必要かも。
このQuery
オブジェクトを引数に使ってDatastoreService#prepare(Query query)
を実行する事でPreparedQuery
が返されます(次の項で説明)。
- Kindを指定するコンストラクタ
Query query = new Query(String kind)
- Kindと親キーを指定するコンストラクタ
Query query = new Query(String kind, Key ancestorKey)
- フィルタ
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")
とほぼ同じです。
Query query = new Query("kind") .addFilter("height", FilterOperator.EQUAL, 100) .addSort("height", SortDirection.ASCENDING) .addSort("__key__", SortDirection.ASCENDING);
addFilter()
, addSort()
はthis
を返してくれるのでチェインして記述する事ができます。
コンストラクタに親キーのみ指定するものもあるんですが、それを使って何かが取得できた記憶が無いので使い方がよくわかってません。コレの使いどころがわかっている方、教えて下さると嬉しいです。
読み出す(2)
上の項目で説明したQuery
を引数にしてDatastoreService#prepare(Query query)
を実行する事でPreparedQuery
が取得できるのですが、ここから「何を/どのように取得するか?」といったカンジになります。
重要な点として、1000件以上の結果は返せないという点です。とはいえ1000件以上取得できるJDOも、1000件以上を扱おうとすると遅くて実用的ではない事も多いので、何が何でも1リクエストに対して1000件以上を一気に!…って時は色々工夫が必要になります。工夫した結果、low-level APIにたどり着くのかもしれません。
FetchOptions
後述する、「結果セットをどのように取得するか?」で使用する取得メソッドで使用します。個人的にはasList()
メソッドで必要だから常にoffset(0)
を使っている程度で、あまり使いこなせていないので細かい説明ができません、ゴメンナサイ。でも大体名前の通りなんでしょう。
FetchOptions offset(int offset)
FetchOptions limit(int offset)
FetchOptions chunkSize(int offset)
FetchOptions prefetchSize(int offset)
結果セットをどのように取得するか?は以下のパターンがあります。一番良く使うのはasList()
かな?
List<Entity> asList(FetchOptions fetchOptions)
Iterable<Entity> asIterable([FetchOptions fetchOptions])
Iterator<Entity> asIterator([FetchOptions fetchOptions])
結果セット以外に取得できる物として、以下があります。
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だけが返される事を期待するよね?)。
Transaction
これはJDOの時と似たような使い方が出来ます。コメントでひがさんより指摘を頂き、最初と比べて一部修正しています。put()
メソッドの第一引数にtransaction
を指定しています。
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(); } }
大体こんなカンジで実用できると思います。low-level APIも結構かわいいやつなので使ってやって欲しいと思います。JDOと併用するのも良い方針だと思います。例えばJDOメインだけどマスタ系へのアクセスは常にlow-level APIとか、まぁ色々と考えられると思います。
追記
Relationshipについて補足する続編も書きました。
8 件のコメント:
子供のputも引数にトランザクションが必要だと思います。
コメントありがとうございます。
> putの引数にトランザクションは必要
ここは悩んでいる所なんです。put(Transaction tx, Entity entity)とput(Entity entity)の差が見当たらないんですよね。pythonでいうrunInTransactionな方法に慣れていると、transactionを渡したくなるんですが、渡さなければならないという理由が見当たらんので困ってます(どーもTransaction内で動作しているよぅに見える)。
「Transactionを渡しておくのが無難」というカンジで理解する方が良いのでしょうか。記事の方は修正を入れます。
put(Transaction txn, Entity entity) のjavadocに
Exhibits the same behavior as put(Entity), but executes within the provided transaction.
と書かれているので、トランザクション内で実行したい場合は、put(Transaction txn, Entity entity)を呼び出すほうがいいと思います。
呼び出さないとトランザクションに参加できないんじゃないかなぁ。
ありがとうございます。
ドキュメントに従って第一引数にTransactionを渡す方法で固定していこうと思います。
Queryの「コンストラクタに親キーのみ指定するもの」ですが、Kindにかかわらず、共通して指定したキーをAncestorとして持つエンティティが取得できるのではないでしょうか?
aエンティティの子として作られたBカインドのbエンティティとCカインドのcエンティティ、…のような。
確かめていないのでただの予想ですが。
>> すぎゃーん
もちろんその予想で試してみたのですが、なーーんも帰って来てくれなかったのです。エラーにもならなかったです。
とはいえ試したのは1.2.0の頃だったかもしれんので、1.2.2でも試してみるとします。
そうだったのですか…確かめもせずに適当なことを言ってすみません ><
いえ、気になった点はどんどんコメントしてもらった方がこちらも参考になって嬉しいです。
今回の件ならQuery(Key ancestorKey) が何も返してくれないサンプルコードを書いた方がわかりやすそーだ、という事に気づきました!
他にも随所でサンプルコードを挟んだ方がわかりよいんだろーなーと反省中。
コメントを投稿