たごもりすメモ

コードとかその他の話とか。

TransactionとEntity Groupについての理解

ドキュメントや各所のblog、TwitterのTLなどを眺めていると、TransactionとEntity Groupについての理解で混乱することがあったので注記しておく。production環境のDatastoreで動作を確認するまで確信できなかった。

TransactionとEntity Groupについて一般的に言われること

  • トランザクション内ではひとつのEntity Group内のEntityしか更新できない
    • 異なるEntity GroupのEntityは別のTransactionで処理してね
    • Entityふたつの間で一貫性が欲しければ同じEntity Groupにするしかないよ
  • Entity Groupを大きくしすぎるとパフォーマンスが落ちる
    • Entity Groupが同じデータはDatastore上でのマスタノードが共通になるので、そこに負荷が集中する
    • オフィシャルのドキュメントを読むと「Entity Groupはひとり分のデータのサイズにおさえておくのがよい」とある

あれ? じゃあ新しいEntityを追加するときは?

あるEntityを追加するとき、例えば顧客名を key_name にセットした以下のようなEntityを考えてみる。

class UserAccount(db.Model):
   fullname = db.StringProperty()

user1 = UserAccount(key_name="tagomoris", fullname="Tagomori Satoshi")
user1.put()

何も考えなければこれでいい。

さて、この "tagomoris" というUserAccountが追加されようとした瞬間に、同時に以下のようなコードが実行されようとしている可能性を考えてみる。

user2 = UserAccount(key_name="tagomoris", fullname="Tagomori SSSSSSS")
user2.put()

何も制御が行われていなければ両方のコードが成功し、そして前にput()されたデータは失われる。
で、そういった状況を回避するためにTransactionの登場だ。

でも、あれ? 待てよ。Transactionは「同じEntity GroupのEntityしか扱えない」んじゃなかったっけ? ここで user1 と user2 は両方とも parent 属性をセットしていない(root entity)。ってことはEntity Groupも別だから、次みたいにTransactionを書いてもうまくいかないんじゃないの?

def add_useraccount(name, fullname):
    u = UserAccount.get_by_key_name(name)
    if u is not None:
        raise UserAccountNameAlreadyInUse
    u = UserAccount(key_name=name, fullname=fullname)
    u.put()
    return u

new_user = db.run_in_transaction(add_useraccount, "tagomoris", "Tagomori Satoshi")

結論から書くと、このコードは完璧に正しく期待通りに動作する。そしてこのコードを経由して作成した各Entityは、きちんと別々のEntity Groupに所属することになる。

手で衝突を起こしてみたときに表示される以下の警告から、その理由がわかる。

WARNING:root:Transaction collision for entity group with key datastore_types.Key.from_path(u'UserAccount', u'tagomoris', _app_id_namespace=u'application_id')

つまりTransaction内では、(多分Keyに関する操作がある場合には)暗黙のうちに「Key.from_path(u'UserAccount', u'tagomoris')を親とするEntity Groupに対する操作」として扱われている、ということだろう。なので、特定の named key (key_nameで指定されたKey)を用いたput()処理はTransaction内で正常に衝突検出できている、ということだ。
この暗黙の関係はKeyに関係する操作がある場合(つまりおそらくput()とdelete()がある場合。delete()はphantom readになるだけなので違うかも?)のみなので、通常のEntity操作ではきちんと別のEntity Groupとして扱われ、並列分散処理が可能となる。

ま、アレだ。新しいEntityのinsert時だけ気をつけようぜ、ってことで。