前のエントリー で書いたように、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 , MonthCountobj について一貫性が保たれなければならない。しかし、 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 が実行されている。その引数には commitcommit の中では __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 というメソッドでクリアしている。 putdelete をオーバーライドして _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での開発はこのモデルの問題さえつぶしてしまえばかなり楽だし、夢がひろがるなあ。


なんの幸運か、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に移行した人の感想がききたいなあ。


まぁ需要はないと思うんですが一応。内部的には WebService::Simple みたいになってるんでサクッと対応できます。

ダウンロード

yahooapi

使い方

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
#=>
#かな
#佳な
#仮名
#カナ
#カナ
#加奈
#可奈
#佳奈
#香奈
#香菜
#華奈
#花奈
#哉
#金

うむ。