Google App Engineのトランザクション

 GAEのトランザクションについてわからないことが多かったので、いろいろ試してみてまとめてみました。
http://code.google.com/intl/ja/appengine/docs/python/datastore/transactions.html
そのために書いたのが以下のコード。
http://gist.github.com/286805

class TestModel(db.Model):
    message = db.StringProperty()

class TransactionTest():
    def run_transaction_test(self):
        try:
            db.run_in_transaction(self.transaction)
            return True
        except:
            return False

class Test0(TransactionTest):
    def transaction(self):
        a = TestModel(key_name='reni0', message=u'がんばれに')
        a.put()

class TransactionDispatcher(webapp.RequestHandler):
    def get(self, num=''):
        n = int(num)
        transaction_class_list = [
            Test0, Test1, Test2, Test3, Test4,
            Test5, Test6, Test7, Test8, Test9
        ]
        
        if n >= len(transaction_class_list):
            self.redirect('/transaction/index')
        
        t = transaction_class_list[n]()
        if t.run_transaction_test():
            self.response.out.write('<html><body>Test%s is success.</body></html>' % num)
        else:
            self.response.out.write('<html><body>Test%s is error.</body></html>' % num)

class MainHandler(webapp.RequestHandler):
    def get(self):
        w = self.response.out.write
        w('<html><body>')
        for i in range(10):
            w('<p><a href="/transaction/test%s">Test%s</a></p>' %
                (str(i), str(i)))
        w('</body></html>')

application = webapp.WSGIApplication(
    [('/transaction/test([\d]+)', TransactionDispatcher),
     ('/transaction/index', MainHandler)],
    debug=True)

def main():
    run_wsgi_app(application)

if __name__ == '__main__':
    main()

TransactionTestを継承したクラスを作り、その中にtransactionというメソッドを定義。トランザクションの前に初期処理が必要な場合はコンストラクタで実行します。これで開発サーバを起動してTest0のリンクをクリックするとトランザクションが処理されて結果を表示。上のTest0は1つのエンティティしか操作してないのでまず成功。リファレンスには「1 つのトランザクションでのデータストア操作は、すべてが同じエンティティグループ内のエンティティを対象とします。これには、キーによるエンティティの取得、エンティティの更新、エンティティの削除が含まれます。」とあるので以下の3つのトランザクションもすべて失敗する。

class Test1(TransactionTest):
    def transaction(self):
        a = TestModel(key_name='shiorin1', message=u'しおりんうれしおりん')
        a.put()
        b = TestModel(key_name='kanako1', message=u'かなこぉ↑')
        b.put()

ルートエンティティを2個以上作ることはダメ。

class Test2(TransactionTest):
    def __init__(self):
        a = TestModel(key_name='shiorin2', message=u'しおりんうれしおりん')
        a.put()
    def transaction(self):
        a = TestModel.get_by_key_name('shiorin2')
        b = TestModel(key_name='odagirishiorin2', message=a.message)
        b.put()

エンティティの取得もダメ。

class Test3(TransactionTest):
    def __init__(self):
        a = TestModel(key_name='kanako3', message=u'かなこぉ↑')
        a.put()
    def transaction(self):
        a = TestModel.get_by_key_name('kanako3')
        a.delete()
        b = TestModel(key_name=u'momoka3', message=u'有安でありやす')
        b.put()

エンティティの削除もダメ。つまりトランザクション内では複数のルートエンティティの操作はまったくできないということ。じゃあ、どうすればよいかというとトランザクション内で一緒に操作したいエンティティはエンティティグループとしてまとめる。エンティティグループとしてまとめるには親エンティティの指定が必要となる。その親エンティティと子エンティティの繋がったまとまりがエンティティグループとして定義される。つまり、

class Test4(TransactionTest):
    def transaction(self):
        a = TestModel(key_name='shiorin4', message=u'しおりんショック><')
        a.put()
        b = TestModel(key_name='odagirishiorin4', message=u'お゛た゛き゛り゛し゛お゛り゛です', parent=a)
        b.put()

このコードならトランザクション成功となる。2個目のエンティティ生成の際に引数でparent=aと1個目のエンティティを指定して、2つのエンティティに親子関係を結びつけている。これでこの2つのエンティティは同じエンティティグループとなる。

class Test5(TransactionTest):
    def transaction(self):
        a = TestModel(key_name='shiorin5', message=u'滅相もない')
        a.put()
        b = TestModel(key_name='odagirishiorin5', message=u'お゛た゛き゛り゛し゛お゛り゛です', parent=a)
        b.put()
        c = TestModel(key_name='uran5', message=u'う゛ら゛ん゛です', parent=a)
        c.put()

また、子エンティティはいくつでも作れる。なので上のコードも成功する。エンティティグループが設定されているエンティティは取得する際に親エンティティの情報が必要となり、以下のように指定してあげなければいけない。

class Test6(TransactionTest):
    def __init__(self):
        a = TestModel(key_name='shiorin6', message=u'さびしおりん')
        a.put()
        b = TestModel(key_name='odagirishiorin6', message=u'お゛た゛き゛り゛し゛お゛り゛です', parent=a)
        b.put()
        c = TestModel(key_name='uran6', message=u'う゛ら゛ん゛です', parent=a)
        c.put()
    def transaction(self):
        a = TestModel.get_by_key_name('shiorin6')
        b = TestModel.get_by_key_name('odagirishiorin6', a)
        b.message = u'に゛ゃー'
        b.put()
        c = TestModel.get_by_key_name('uran6', a)
        c.message = u'わ゛ぁー'
        c.put()

ここで親エンティティが削除されたら、子エンティティにアクセスできなくなるかというとそうではなく、子エンティティにはまだ親エンティティの情報が残されてる。ただ親エンティティの実体がないだけ。そこでdb.Key.from_pathで親エンティティのキーだけ生成して、それで子エンティティにアクセスする。それが以下のコード。これも成功する。

class Test7(TransactionTest):
    def __init__(self):
        a = TestModel(key_name='shiorin7', message=u'しおりんショック><')
        a.put()
        b = TestModel(key_name='odagirishiorin7', message=u'お゛た゛き゛り゛し゛お゛り゛です', parent=a)
        b.put()
        c = TestModel(key_name='uran7', message=u'う゛ら゛ん゛です', parent=a)
        c.put()
        a.delete()
    def transaction(self):
        parent_key = db.Key.from_path('TestModel', 'shiorin7')
        b = TestModel.get_by_key_name('odagirishiorin7', parent_key)
        b.message = u'に゛ゃー'
        b.put()
        c = TestModel.get_by_key_name('uran7', parent_key)
        c.message = u'わ゛ぁー'
        c.put()

また、エンティティグループは親→子→孫のように縦に繋ぐことも可能。なので以下のコードも成功する。

class Test8(TransactionTest):
    def transaction(self):
        a = TestModel(key_name='shiorin8', message=u'しおりんショック><')
        a.put()
        b = TestModel(key_name='odagirishiorin8', message=u'お゛た゛き゛り゛し゛お゛り゛です', parent=a)
        b.put()
        c = TestModel(key_name='ruka8', message=u'るかです', parent=b)
        c.put()

この場合、親→子→孫の子エンティティを削除してもdb.Key.from_pathで子エンティティのキーを生成して孫エンティティにアクセスできる。なので以下のコードは成功する。

class Test9(TransactionTest):
    def __init__(self):
        a = TestModel(key_name='shiorin9', message=u'しおりんショック><')
        a.put()
        b = TestModel(key_name='odagirishiorin9', message=u'お゛た゛き゛り゛し゛お゛り゛です', parent=a)
        b.put()
        c = TestModel(key_name='ruka9', message=u'るかです', parent=b)
        c.put()
        b.delete()
    def transaction(self):
        a = TestModel.get_by_key_name('shiorin9')
        a.message = u'悲しおりん'
        a.put()
        b = db.Key.from_path('TestModel', 'shiorin9', 'TestModel', 'odagirishiorin9')
        c = TestModel.get_by_key_name('ruka9', b)
        c.message = u'悲しおりん'
        c.put()

 と、こんな感じでトランザクションとエンティティグループの関係についてなんとなく理解出来た。親エンティティを削除してもKey.from_pathでキー生成して子エンティティにアクセスできるというのが実体はないけど幽霊を使って処理してる感じで不思議。あと、更新、削除だけでなくエンティティの取得も同じエンティティグループに限られるのが制限としてきついなと思う。まあ、取得はトランザクションに含めなければいいだけなんだけど。