火曜日, 7月 28, 2009

【Google App Engine】 Pagingをどうやって実現するか


 JDOをそのまま使うとPagingができないことは前記事で述べたとおりだが、DatastoreAPIを使えば可能なので、今回はその方法について述べたいと思う。前記事と同様、請求書アプリを元に説明する。

KeyとCounter


 まず、基本となるEntityのInvoiceBaseとInvoiceの構造から。



 InvoiceBaseは、Invoiceレコードを保持するする親のEntityだ。InvoiceBase(親)に紐づくInvoice(子)は1:nのOwnedな関連である。また、Invoiceレコードの件数を格納するcounterプロパティをもつ。InvoiceのKeyは、トランザクションで括る必要があってKeyを連結させているが、拙作ライブラリReflex GAEのAPI、keyUtils.getChildKey()を使って、連結されたInvoiceのKeyを取得している。(ReflexGaeライブラリは=>ココ
 レコードの件数counterは、Pagingのために必要な連番を振るためにも使用される。Datastoreは自動で連番を付けられないため、InvoiceBaseの自身でもつより他ない。

 一応、JDOのドキュメントで以下のようなものがあったので試してみたが案の定できなかった。(追記:最新版SDK1.2.6では、Statics APIで件数が取れるようになっている。こちらの記事の参考に=>Keyとカウンタは別々に考えるといいかも


【コード】
 @PrimaryKey
 @Persistent(valueStrategy = IdGeneratorStrategy.INCREMENT)
 private Long uid;

【エラー内容】
 Message : There is no available value generator for strategy "increment" for this datastore. Please consult the documentation for details of which generators are available.


 そもそも、親子関係をもつようなEntityを扱うには、Long型ではなくKey型を使わなくてはならない。Googleのドキュメントにも記述されているが、Keyの中身が以下のようなツリー構造をしていることからもその理由は想像できる。


子要素までaddしたときのキー(実際はBase64でエンコードされている)

 j reflexworksr,  InvoiceBase" InvoiceBase Invoice"ID2
 j reflexworksr,  InvoiceBase" InvoiceBase Invoice"ID1


 よく、KeyをStringとしてもたせるような記述、「@Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true")」を見かけるが、階層型の場合、連結させたDatastoreのKeyがそのままStringに変換されてしまうのでおもしろくない。例えば、先のEntityのKeyをStringにするとその中身は、「j reflexworksr,  InvoiceBase" InvoiceBase Invoice"ID1 」となってしまう。業務アプリで意識すべきは、業務アプリのKeyであって、DatastoreのKeyではない。DatastoreのKeyはむしろ隠蔽させて、ID1という業務アプリのキーだけを意識させる方がわかりやすいだろう。なので、Reflexでは、JDO KeyをStringとしてもたせる記述はせず、アプリのPKをKeyに変換するという方法をとっている。以下はアプリのPKであるinvoiceNoをJDOのKeyに変換する例である。(keyUtilsはReflexGaeのAPI)


 Key childKey = keyUtils.getChildKey(Invoice.class, param.invoiceNo);


Keyによる検索

 このPKを使った検索の例は次のとおり。pm.detachCopy()をすることで、Persistentではない、自由に扱えるEntityのコピーを得ることができる。Persistentなものは、まだDatastoreにAttachされていて、コネクションが張られている状態のような感じ。実際に値を書き換えるだけでDatastoreの中身も変更される。



Paging検索

 Paging検索では、以下のように、idで開始点を与えて、かつ、withLimitで範囲を絞るといった感じになる。JDOでは6千件でフリーズしてしまったが、DatastoreAPIを使っているため理論的には無限で、このサンプルではcounterの最大値(longの2147483647件)までは扱えることになる。とりあえず1万件でテストしてみたが非常に高速にレスポンスが返ってきたので問題なく使えると思う。

 1) filterで開始点となるidの値をセットして、それ以上のものを検索対象とする

query.addFilter("id", Query.FilterOperator.GREATER_THAN , Long.parseLong(nextId));

 2) FetchOptionsでwithLimit()をつけることで、検索結果の件数を絞る

FetchOptions fetchOptions = FetchOptions.Builder.withLimit(limit);




指定可能なfilter条件

 DataStoreAPIでは、GREATER_THANなどのinequality filter(<,>など)が1つと、equality filter(=)を複数指定できる。連番でinequality filterを使ってしまっているため、あとはequality filterだけが使えることになる。Reflex Gaeライブラリでは、QueryUtilsというものを用意していて、検索条件を格納したParameterBeanからequality filterを自動的に追加できるようにしている。

 // 検索項目(Indexがあるもの) を指定してnewする
 QueryUtils queryUtils = new QueryUtils(new String[]{"invoiceNo","companyName","job","issuedDate"});

// paramの検索項目がnullでなければaddFiler(FilterOperator.EQUAL)される
queryUtils.setParam(param, query);


EntityConverterによる変換

 DatastoreAPIで検索すると、MAPで返ってくるのでEntityのPropertiesからListに変換する必要がある。DatastoreAPIには、DataTypeTranslatorというのがあったが、使い方がよくわからなかったので自前で作成した。それがEntityConverterである。

List result = entityConverter.convert(Invoice.class, resultIterable,null,condition);


 filterで指定できる条件はequality filterだけなので、Like検索など複雑なことをやりたい場合には、Entityを取得した後で別途Java側で処理しなければならない。EntityConverterにソート機能や条件抽出機能をもたせたているのはこういった理由からだ。
 comparatorをEntityConverterの第三パラメータに与えることでEntityをソートして返すことができる。また、EntityConverterの第4パラメータにconditionを与えることで、conditionに合致するものだけを変換対象にするといったことができる。
 
Indexについて

 上記ソースのQueryはDatastoreAPIのものであってJDOQLではないので注意が必要だ。JDOQLでは自動的にIndexが作成されるが、DatastoreAPIではindexがないとエラーになるので自分で作成する必要がある。(これはむしろ好都合)
 作成するには、すべての検索パターン分のindexを定義した、datastore-indexes-auto.xmlを所定の位置(WEB-INF/appengine-generated)の下に置いてDeployするだけだ。(datastore-indexes.xmlではダメだった。また、index作成には結構時間がかかるので辛抱づよく待つ必要がある。管理画面のDatastore=>Indexesで、StatusがServingになればOKだ。とにかく待とう)
 すべての検索パターン分のindexとは、検索条件の組み合わせをすべて定義しなければならないという意味である。例えば、請求書アプリでは、"id"と"companyName"の2つの条件で検索したい場合もあれば、"invoiceNo","companyName","invoiceNo","issuedDate"の4つで検索したい場合もある。検索する可能性の組み合わせすべてを定義しなければならない。



登録処理 

 登録では次のような感じになる。



大規模なデータをもつ子要素に対してInsertして問題ないか

 まず、pm.getObjectById()でinvoiceBaseを取得する。存在しなかったら新規作成して、pm.makePersistent(invoiceBase)を実行する。これは更新時には必要ないため新規作成時のみ実行するようにする。Invoiceレコードを追加するたびに、InvoiceBaseのcounterをインクリメントする。
 InvoiceレコードはinvoiceBaseの子要素であるが、いわゆる、eager loadではないため、pm.getObjectById()しただけでは、Invoiceの全レコードがメモリに乗ることはない。getter/setterでアクセスしたものだけが実際のDatastoreへのアクセス対象となるので、膨大なレコード数が対象であってもこれでOKだ。(実際に1万件に対して実行しても大丈夫だった)

パフォーマンス

 パフォーマンスへの影響を考えた場合、EntityGroupは最小化すべきとよく言われる。実際、この例はテーブルロックのイメージに近いため、RDB設計に携わってこられた方は、どうしても気になるところだろう。counterとレコードの関係を考えると、トランザクションは切り離せない部分であるため、どうしてもこのような実装になってしまうが、それでも、コンテンションが起きた場合は、JDOCanRetryExceptionを拾ってリトライできるので、スループットは出せるのではないかと思う。実際に測定してみたところ、遅くなる要因としては、EntityGroupの大きさというより、トランザクション処理の件数の方がインパクトが大きかったので、TaskQueueとMemcacheを駆使して、複数のトランザクションを1つにまとめて非同期に一括処理するようなものを介すことでパフォーマンスを改善できると思われる。このあたりの話は深いのでまた別の機会に詳しく述べたいと思う。
 ちなみに、上記コード中で、JDOCanRetryException内で都度rollbackしているのは、そうしないとうまく動作しなかったからだ。(Documentどおりだと動作しないと思われるので注意)

楽観的ロック

 同一レコードの書き換えでコンテンションが起きてしまう場合は楽観的ロックが有効である。@Version(strategy = VersionStrategy.VERSION_NUMBER)を使用する方法があるようだが、この程度のものは潰しがきくので自前で実装する方がよい。具体的には、Updateの際にrevisionを比較して同じであれば更新実行してrevisionを+1するだけ。比較して違う場合にエラーとすればよい。

<関連>
Entityとトランザクション
JDOから直接JSON、XML
JDOから直接SOAP、ATOM。それからDeep Copy
AJAX CRUDサンプルとJDO代替ライブラリ
RESTfulアプリのCRUDサンプル -Servlet編-
RESTfulアプリのCRUDサンプル -Modeling編-

0 件のコメント:

コメントを投稿