前のエントリー で書いたように、GoogleAppEngineのトランザクションは使い勝手が悪い。しかし、GAEにブログを移行しようと思うとこれは乗り越えなければならない。
GAEでトランザクションを行う条件を簡単にまとめると
- db.run_in_transactionにトランザクションとして実行したい関数を渡すがその関数内では
- 同じエンティティグループに属しているモデルで
- get, put, deleteのみ
しか実行できない。なのでトランザクションのページに紹介されているような
from google.appengine.ext import db
class Accumulator(db.Model):
counter = db.IntegerProperty()
def increment_counter(key, amount):
obj = db.get(key)
obj.counter += amount
obj.put()
q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()
db.run_in_transaction(increment_counter, acc.key(), 5)
先にGQLを発行しておいて、 run_in_transaction
に渡す関数ではGQLの結果として取得した key
を引数にとる、という回りくどいコードになる。コレは書きづらい。
これも前のエントリーに書いたように、今回作ったブログアプリでは月別アーカイブやタグアーカイブの記事数をエントリのCRUD時に操作している。
エントリ作成時なら
@classmethod
def create(cls, *a, **k):
obj = super(Entry, cls).create(*a, **k)
TagCount.inc(obj.status, *obj.tags)
MonthCount.inc(obj.status, obj.created_month)
return obj
というような感じだ( status
は公開、とか非公開とかが入る)。当然、この create
メソッドでは TagCount
, MonthCount
と obj
について一貫性が保たれなければならない。しかし、 TagCount.inc
では対象のタグを検索して、なければ作成し put
する、という操作を行う。「対象のタグを検索して」というトランザクション内で許可されていない操作が入っているのだ。
さて、こういうときはとりあえず?力技で乗り切ろう。かなり強引だが、下のようなコードで乗り切ってみた。一応、サーバにアップロードして動作することは確認している。
class RootModel(db.Model):
id = db.IntegerProperty(default=1)
def __call__(self, k):
v = k or {}
v.update(parent=with_parent)
return v
with_parent = RootModel.get_or_insert("id", id=1)
まず、全てのエンティティを同一エンティティグループに所属させることにする。そのため、全てのエンティティの親となるエンティティを作成する。
次は強引さの根源のようなクラスだ。
class Transaction(object):
MAGIC_NAME = "__magic__lst__"
SEARCH_MAX = 12
def __init__(self, f):
self.f = f
def execute(self):
__magic__lst__ = []
result = self.f()
def commit():
for dbop in __magic__lst__:
dbop()
return True
committed = db.run_in_transaction(commit)
if committed is None:
raise db.Rollback()
return result
これだけでは分からないと思うが、 execute
内で run_in_transaction
が実行されている。その引数には commit
。 commit
の中では __magic__lst__
というリスト内の関数を実行しているようだが、 __magic__lst__
に要素が追加されている形跡がない。
そして、全てのモデルの親となる基本モデルを定義する。
class BaseModel(db.Model):
@classmethod
def create(cls, *a, **k):
obj = cls(*a, **with_parent(k))
obj.put()
return obj
def _with_transaction(self, name):
i = 1
f = sys._getframe(i)
while f and i < Transaction.SEARCH_MAX:
if Transaction.MAGIC_NAME in f.f_locals:
f.f_locals[Transaction.MAGIC_NAME].append( \
lambda : getattr(super(BaseModel, self), name)())
return
i += 1
f = sys._getframe(i)
return getattr(super(BaseModel, self),name)()
def put(self):
return self._with_transaction("put")
def delete(self):
return self._with_transaction("delete")
def update(self, **k):
for prop in self.properties().values():
if prop.name in k:
prop.__set__(self, k[prop.name])
self.put()
@classmethod
def get_by_id(cls, id):
return super(BaseModel, cls).get_by_id(id, parent=with_parent)
この基本クラスではまず、 create
というメソッドを定義し、エンティティはすべてこのメソッドを通して作成するようにしている。 create
では必ず親エンティティとして with_parent
を指定する。これにより全てのエンティティは同一エンティティグループに属することになる。
- 同じエンティティグループに属しているモデルで
という条件はクリアしたことになる。次は
- get, put, deleteのみ
という条件だ。これは _with_transaction
というメソッドでクリアしている。 put
と delete
をオーバーライドして _with_transaction
を呼び出すようにしている。さて、この _with_transaction
ではフレームをさかのぼって、 __magic__lst__
という名前のローカル変数が存在するフレームがないか探索する。そのフレームが存在した場合、一連のトランザクション内での実行とみなし、 __magic__lst__
に put
, delete
を行うthunkを登録し、終了する。存在しない場合は通常の実行ということで、その場で put
, delete
を行う。つまり、トランザクション内で実行されたことを検出して put
, delete
の評価を遅延させるのだ。
さきほどの create
は
@classmethod
def create(cls, *a, **k):
def _():
obj = super(Entry, cls).create(*a, **k)
TagCount.inc(obj.status, *obj.tags)
MonthCount.inc(obj.status, obj.created_month)
return obj
return Transaction(_).execute()
と若干のコード挿入のみできちんと一貫性が保証されるようになる。 ********************************* というわけで、かなり強引な気がするけどトランザクションの問題は一応クリアされた。めんどくさければHTTPリクエストを受けてから、レスポンスを返すまでを関数でラップし、 Transaction.execute
すれば大丈夫だ。
ここまでやってしまったので、やっぱり今のブログはGAEに移行しようかなあ、と思い始めたり。デザインも今と似ているけどやっぱ一から自分でマークアップしたから好みだし、なによりXHTMLとしてValidだ。機能的にはあと、スパム対策さえあれば大丈夫。
実はOpenIDも自分でサーバ立ててたりするので、それもGAEで実装しなおしたいなあとか色々やりたいことは尽きない。GAEでの開発はこのモデルの問題さえつぶしてしまえばかなり楽だし、夢がひろがるなあ。