GoogleAppEngineのトランザクションをねじふせる
前のエントリーで書いたように、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での開発はこのモデルの問題さえつぶしてしまえばかなり楽だし、夢がひろがるなあ。
GoogleAppEngineのモデルをいじってみた
なんの幸運か、Softbank(しかもJ-phone時代からの)ユーザだったので、SMS認証もアッサリ通ってGAEのアカウントをゲットしたわけですが・・・(ただしiPhoneには興味ほぼなし)。
というわけでishikawaさんがブログをGAEに移行された、というのを知って俺も移行しようかなあとか思いつつブログアプリ作りました。すでに今ブログで使っている機能は全て実装済みだったりします。デザインも全頁XHTMLTr validでCSSもvalidなことを確認済みだったりします。しかもDjangoじゃなくてweb.pyだったりします。慣れてるしね。
それはおいといて、データストアまわりがそのままだと
filter("hoge =", value)が長ったらしい- CRUDが微妙。
- フォーム作成用等に空のモデルを一時的に作成したいけど、作成した時点でvalidateされるので作成できない
らへんがいまいちだったで、ちょっといじってみました。
- def _getattr(self, name):
- if name.endswith("_eq"):
- return lambda v: self.filter("%s ="%name[:-3], v)
- raise AttributeError(name)
- setattr(db.Query, "__getattr__", _getattr)
- class ModelMixin(object):
- default_order = None
- _stab_class = None
- @classmethod
- def stab(cls):
- if not cls._stab_class:
- stab = type('Stab%s'%cls.__name__, (cls,), dict())
- for k,v in stab._properties.iteritems():
- v.required = False
- v.choice = None
- v.validator= None
- cls._stab_class = stab
- stab = cls._stab_class()
- for k,v in stab._properties.iteritems():
- v.__set__(stab, v.default_value())
- return stab
- @classmethod
- def create(cls, *a, **k):
- obj = cls(*a, **k)
- obj.put()
- return obj
- @classmethod
- def find_all(cls):
- result = cls.all()
- if cls.default_order is not None:
- return result.order(cls.default_order)
- return result
- def update(self, **k):
- for prop in self.properties().values():
- if prop.name in k:
- prop.__set__(self, k[prop.name])
- self.put()
- def destroy(self):
- self.delete()
これで
- class Test(ModelMixin, db.Model):
- name = db.StringProperty(require=True)
- created_at = db.DatetTmeProperty(auto_now_add=True)
- default_order="-created_at"
- Test.find_all() # created_atの降順
- Test.all().name_eq("user").update(created_at = datetime.now())
- stab = Test.stab() #nameがなくても作成できる
こんな感じにかけます。
さて、そんなこんなでブログアプリを作ったのはいいんだけど、なんとなく移行する気にならない。現在はWordpressで運用しているけど、正直セキュリティホールは多いし、ソースコードを見ると泣きたくなるようなコードだし、頻繁なバージョンアップはめんどくさい・・・。プラグインのセキュリティホールまで考えるとなおめんどくさい。しかもそこまで機能を使い倒しているわけじゃないし。
だからGAEにでも移行したいんだけど・・・。データがGoogleに握られるのはそこまで心配じゃない。ブログサービスでブログをやれば、そのブログサービス事業者にデータを握られるってのと同じだから気にならないんだな。移行作業自体も記事数がかなり少ないのでそんなにめんどくさそうではない。
ひとつ引っかかっているのはトランザクションの問題。まぁめったにトランザクションで問題はおきないと思うけど、GAEのトランザクションは一応ある、といった程度でほとんど使えない。使おうとしたらtransactionの中でこれはしちゃだめ、これもしちゃだめ、とおこられまくったので現在トランザクション管理ができていない。GAEには集約系のメソッドがないので、月別記事数とか、タグ別記事数とかを記事のCRUD時に別モデルで管理しているわけだけど、ここのトランザクションが管理できないのは結構不安。
もうひとつはなんとなーく使い切らないんだけど無料の上限である500Mという容量が微妙。画像のアップとかしなけりゃまず大丈夫なんだけど。
その他にもなんとなくGoogleの罠じゃないかとかいろいろ気になって移行に踏み切れないでいる今日この頃。実際に本格的にGAEに移行した人の感想がききたいなあ。
Python版Yahooテキスト解析 APIライブラリをかな漢字変換に対応させました
まぁ需要はないと思うんですが一応。内部的にはWebService::Simpleみたいになってるんでサクッと対応できます。
ダウンロード
使い方
- import yahooapi.jlp as jlp
- client = jlp.JIMServiceAPI("your_apikey")
- result = client.conversion(sentence=u"かなかんじへんかんたいしょうのてきすとです")
- for i in result.Result.SegmentList.Segment[0].CandidateList.Candidate:
- print i
- #=>
- #かな
- #佳な
- #仮名
- #カナ
- #カナ
- #加奈
- #可奈
- #佳奈
- #香奈
- #香菜
- #華奈
- #花奈
- #哉
- #金
うむ。