GoogleAppEngineのトランザクションをねじふせる

前のエントリーで書いたように、GoogleAppEngineのトランザクションは使い勝手が悪い。しかし、GAEにブログを移行しようと思うとこれは乗り越えなければならない。

GAEでトランザクションを行う条件を簡単にまとめると

  • db.run_in_transactionにトランザクションとして実行したい関数を渡すがその関数内では
  • 同じエンティティグループに属しているモデルで
  • get, put, deleteのみ

しか実行できない。なのでトランザクションのページに紹介されているような

python code
  1. from google.appengine.ext import db
  2.  
  3. class Accumulator(db.Model):
  4.   counter = db.IntegerProperty()
  5.  
  6. def increment_counter(key, amount):
  7.   obj = db.get(key)
  8.   obj.counter += amount
  9.   obj.put()
  10.  
  11. q = db.GqlQuery("SELECT * FROM Accumulator")
  12. acc = q.get()
  13.  
  14. db.run_in_transaction(increment_counter, acc.key(), 5)
  15.  

先にGQLを発行しておいて、run_in_transactionに渡す関数ではGQLの結果として取得したkeyを引数にとる、という回りくどいコードになる。コレは書きづらい。

これも前のエントリーに書いたように、今回作ったブログアプリでは月別アーカイブやタグアーカイブの記事数をエントリのCRUD時に操作している。

エントリ作成時なら

python code
  1.   @classmethod
  2.   def create(cls, *a, **k):
  3.     obj = super(Entry, cls).create(*a, **k)
  4.     TagCount.inc(obj.status, *obj.tags)
  5.     MonthCount.inc(obj.status, obj.created_month)
  6.     return obj
  7.  

というような感じだ(statusは公開、とか非公開とかが入る)。当然、このcreateメソッドではTagCount,MonthCountobjについて一貫性が保たれなければならない。しかし、TagCount.incでは対象のタグを検索して、なければ作成しputする、という操作を行う。「対象のタグを検索して」というトランザクション内で許可されていない操作が入っているのだ。

さて、こういうときはとりあえず?力技で乗り切ろう。かなり強引だが、下のようなコードで乗り切ってみた。一応、サーバにアップロードして動作することは確認している。

python code
  1. class RootModel(db.Model):
  2.   id = db.IntegerProperty(default=1)
  3.   def __call__(self, k):
  4.     v = k or {}
  5.     v.update(parent=with_parent)
  6.     return v
  7. with_parent = RootModel.get_or_insert("id", id=1)
  8.  

まず、全てのエンティティを同一エンティティグループに所属させることにする。そのため、全てのエンティティの親となるエンティティを作成する。

次は強引さの根源のようなクラスだ。

python code
  1. class Transaction(object):
  2.   MAGIC_NAME = "__magic__lst__"
  3.   SEARCH_MAX = 12
  4.   def __init__(self, f):
  5.     self.f = f
  6.  
  7.   def execute(self):
  8.     __magic__lst__ = []
  9.     result = self.f()
  10.     def commit():
  11.       for dbop in __magic__lst__:
  12.         dbop()
  13.       return True
  14.     committed = db.run_in_transaction(commit)
  15.     if committed is None:
  16.       raise db.Rollback()
  17.     return result
  18.  

これだけでは分からないと思うが、execute内でrun_in_transactionが実行されている。その引数にはcommitcommitの中では__magic__lst__というリスト内の関数を実行しているようだが、__magic__lst__に要素が追加されている形跡がない。

そして、全てのモデルの親となる基本モデルを定義する。

python code
  1. class BaseModel(db.Model):
  2.   @classmethod
  3.   def create(cls, *a, **k):
  4.     obj = cls(*a, **with_parent(k))
  5.     obj.put()
  6.     return obj
  7.  
  8.   def _with_transaction(self, name):
  9.     i = 1
  10.     f = sys._getframe(i)
  11.     while f and i < Transaction.SEARCH_MAX:
  12.       if Transaction.MAGIC_NAME in f.f_locals:
  13.         f.f_locals[Transaction.MAGIC_NAME].append( \
  14.           lambda : getattr(super(BaseModel, self), name)())
  15.         return
  16.       i += 1
  17.       f = sys._getframe(i)
  18.     return getattr(super(BaseModel, self),name)()
  19.  
  20.   def put(self):
  21.     return self._with_transaction("put")
  22.  
  23.   def delete(self):
  24.     return self._with_transaction("delete")
  25.  
  26.   def update(self, **k):
  27.     for prop in self.properties().values():
  28.       if prop.name in k:
  29.         prop.__set__(self, k[prop.name])
  30.     self.put()
  31.  
  32.   @classmethod
  33.   def get_by_id(cls, id):
  34.     return super(BaseModel, cls).get_by_id(id, parent=with_parent)
  35.  

この基本クラスではまず、createというメソッドを定義し、エンティティはすべてこのメソッドを通して作成するようにしている。createでは必ず親エンティティとしてwith_parentを指定する。これにより全てのエンティティは同一エンティティグループに属することになる。

  • 同じエンティティグループに属しているモデルで

という条件はクリアしたことになる。次は

  • get, put, deleteのみ

という条件だ。これは_with_transactionというメソッドでクリアしている。putdeleteをオーバーライドして_with_transactionを呼び出すようにしている。さて、この_with_transactionではフレームをさかのぼって、__magic__lst__という名前のローカル変数が存在するフレームがないか探索する。そのフレームが存在した場合、一連のトランザクション内での実行とみなし、__magic__lst__put,deleteを行うthunkを登録し、終了する。存在しない場合は通常の実行ということで、その場でput, deleteを行う。つまり、トランザクション内で実行されたことを検出してput, deleteの評価を遅延させるのだ。

さきほどのcreate

python code
  1.   @classmethod
  2.   def create(cls, *a, **k):
  3.     def _():
  4.       obj = super(Entry, cls).create(*a, **k)
  5.       TagCount.inc(obj.status, *obj.tags)
  6.       MonthCount.inc(obj.status, obj.created_month)
  7.       return obj
  8.     return Transaction(_).execute()
  9.  

と若干のコード挿入のみできちんと一貫性が保証されるようになる。


というわけで、かなり強引な気がするけどトランザクションの問題は一応クリアされた。めんどくさければHTTPリクエストを受けてから、レスポンスを返すまでを関数でラップし、Transaction.executeすれば大丈夫だ。

ここまでやってしまったので、やっぱり今のブログはGAEに移行しようかなあ、と思い始めたり。デザインも今と似ているけどやっぱ一から自分でマークアップしたから好みだし、なによりXHTMLとしてValidだ。機能的にはあと、スパム対策さえあれば大丈夫。

実はOpenIDも自分でサーバ立ててたりするので、それもGAEで実装しなおしたいなあとか色々やりたいことは尽きない。GAEでの開発はこのモデルの問題さえつぶしてしまえばかなり楽だし、夢がひろがるなあ。

Related posts:

07.27.08/12am

No comments yet

trackback uri
  • ajax-loading
  • ajax-loading
  • ajax-loading

Leave a Comment

You can use these tags: <code>, <i>, <em>, <strong>, <a>

About

Author:yuin(http://inforno.net/)

文学部文化学科卒という生粋の文系趣味プログラマ。

主にRuby、Javascript、PHP、JAVA,Python,C,Scala,Schemeなどを使っています。今はPythonな感じかもしれない。今後作曲活動なども復活するかもしれない。

Pages