2009年9月15日火曜日

#appengine JavaではJDOは捨てましょう!

Slim3のMLに次のようなタイトルの議題が上がっていた。

自分はSlim3は触らないからMLに投稿できないけど、この方向はすごく良い事だと思いました。

スレッド内で少し出ている「App Engine以外では動かなくなる 」は全く問題になら無いと思います。なぜなら、GAE/J用のシステムは設計レベルでGAEに特化してしまうので、結局GAE/J以外に移行するには設計からやりなおす事になり、そーなると実装レベルでJDOを適用して…という話にはならんと思うからです。

low-level APIのメリットその一

これはずっと前からの意見なんですが、まず学習コストが低いですし、BigTableを勘違いせずストレートに理解しやすいんじゃないかと思います。JDOから入ると、従来のORMに使い慣れた人が従来のORMとして使ってしまい、あれ?とはまり込む気がします。特に、JDOのBigTable向け実装の不具合(datanucleus)にはまったりしたら何が何やら?となって最悪です(JPAはもっとヒドイかも)。JDOに関しては細かい状態(persistent, detachedだけではなくpersitent-dirtyとかそんなレベル)がJDOの楽観的排他制御等の処理とからむとかなりややこしいと感じるんですが、この細かい状態の遷移に関してかつて不具合を満載していたりした歴史もあります。ちなみにこれらはJDOという仕様についての話ではなく、JDOの特定の実装の話です、念為。

個人的に最も気に喰わないのは、JDOの仕様よりもはるかに機能が少ないBigTableという実装に対してJDOというインターフェースがかぶさっている事ですね。あえて勘違いを引き起こす為の悪質な罠という気がしてなりません。

low-level APIのメリットその二

最近よく話題になっている、トランザクションの整合性を自前で保証…というような話でもLowLevelAPIは便利に使えます。

制御可能なバッチPUT

SDK1.2.5のリリースと共にJDOでもバッチPUTができるようになるかも?という話があったものの、結局は一部のメソッドだけが対応されただけで、通常のORM的にJDOを使っている人はこの対象から漏れてしまうという残念な結果でした。当然LowLevelAPIは自由自在に制御できます。JDOを使っている人は一回だけ保存操作をしただけのつもりでも、実際にはアプリケーションノードとデータストアサービスノードの間を何往復かしていたりします。

EntityGroupの形成の制限がない

これは私がJDOを理解しきっていないという事で勘違いもあるかもしれませんが、JDOでは同じKind内でのEntityGroupが形成できないんじゃないか?と思います。LowLevelAPIではこれが可能です。例えば、KindA内のA1を親としてA2,A3がその子供としてぶらさがる…といった構造。

SDK1.2.5で追加されたallocateIds()

名前の通り、自動採番時のKeyのIDを割り当ててもらうだけの機能です。トランザクションの外で使用できます。これを使うと、例えば以下のような状況で嬉しいかもしれません。

KindA, KindBがOneToManyで、(A1/B1,A1/B2,A1/B3),(A2/B4, A2/B5,A2/B6)のふたつのEntityGroupにまたがる更新を行いたい
  1. A1/B1,A1/B2,A1/B3をバックアップKind(Backup)に保存する為に、まずはA1用のKeyを確保(Backup(A1)とする)
  2. 次にそのKeyを親として、さらにB1,B2,B3用のKeyを確保(Backup(B1)...)
  3. これでA1,B1,B2,B3,B4をBackupKindに保存する為のKeyが全て揃ったので、バックアップ対象のEntityはシリアライズする等してそれぞれのBackup用Entityの属性に詰め込んでおきます。
  4. 一つ目のEntityGroupのバックアップ用のトランザクションを開始する。
  5. PUT(Backup(A1),Backup(A1)/Backup(B1),Backup(A1)/Backup(B2),Backup(A1)/Backup(B3))する。JDOでこれをやると二回のPUTになってしまいますね。
  6. 一つ目のEntityGroupのバックアップ用のトランザクションをcommitする。

もちろん、この途中で失敗したら、この先の操作の整合性を保証できなくなるため、処理を先に進めないようにエラーを発生させる事になりますね。ポイントは、トランザクション内で一回のPUT操作でバックアップが完了する事です。

  1. 一つ目のEntityGroup更新用のトランザクションを開始する
  2. (A1/B1,A1/B2,A1/B3)を更新する。low-levelAPIオンリーならここも一回のPUT操作で終了ですね。
  3. 正常に処理できれば、commit。

異常が発生した場合はrollbackして、Backup(A1)の削除を行います。low-levelAPIであれば、BackupKindに対してBakup(A1)というAncestorKeyクエリを行えば必要なデータ(Backup(A1),Backup(A1)/Backup(B1),Backup(A1)/Backup(B2),Backup(A1)/Backup(B3)が一発で取得できますので、delete((Backup(A1),Backup(A1)/Backup(B1),Backup(A1)/Backup(B2),Backup(A1)/Backup(B3))するだけですね。ここをTaskQueueで処理できればさらに良いかもしれませんね。

さらに次のEntityGroupでも同様に処理し、もしそっちが失敗した場合はGET(Backup(A1),Backup(A1)/Backup(B1),Backup(A1)/Backup(B2),Backup(A1)/Backup(B3))すれば一回のGET操作で全てバックアップから取得できますね。

こんなカンジに処理を進めて、全て正常終了すればBackup(A1), Backup(A2)をキーにBackupKindを掃除しても良いし、放置しても良いし、TaskQueueで掃除しても良いかもしれませんし、そのあたりはアプリケーションの方針次第。

こんな風に、自前で整合性を維持するための裏方の処理はLowLevelAPIで処理し、極力本来の処理の邪魔をしないようにするのが良いと思います。で、そーこーしているウチにLowLevelAPIだけ使っちゃってる、という事になるんじゃないかなぁ?という風にも思います。

コメントを投稿