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

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

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

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

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

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

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

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

エントリ作成時なら

1@classmethod
2def 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

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

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

1class 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
7with_parent = RootModel.get_or_insert("id", id=1)

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

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

 1class 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

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

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

 1class 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)

この基本クラスではまず、 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

1@classmethod
2def 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()

と若干のコード挿入のみできちんと一貫性が保証されるようになる。     *********   というわけで、かなり強引な気がするけどトランザクションの問題は一応クリアされた。めんどくさければHTTPリクエストを受けてから、レスポンスを返すまでを関数でラップし、 Transaction.execute すれば大丈夫だ。

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

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

comments powered by Disqus