Google App Engine入門:Datastore上で「ユニーク制限」を実現する方法
2009.11.10
Google App Engine のDatastoreには、通常のリレーショナルデータベースと比べた時にいくつかの制限があるが、その一つが「このプロパティの値は常にユニークでなければならない」という指定(ユニーク制限)ができないことである。
Invoice IDのように自動生成するものであれば、アプリケーション側でなんとかすることも簡単だが、メールアドレスやハンドル名など、ユーザーが入力するものになると、ユニークであることをきちんと判定した上でEntityを作ることが必要になる。
もちろん、単純に「有無をチェックして、なければ作る」というプログラムではスレッド間の競合に対応できないので、そこはトランザクションを使ってアトミックに処理をする必要がある。
App Engine上でトランザクションを実現するには、エンティティグループという仕組みを使って行うが、気をつけなければいけないのは、エンティティグループをあまり大きくしてはいけないということ。
すべてのEmailAddressエンティティを一つのエンティティグループに入れてしまえば、トランザクションは簡単に実現できるが、それだとユーザー数が増えたときにエンティティグループが巨大になってしまい、そこがボトルネックになりかねない。
半日ほどいろいろと調査をしたりプロトタイプを作ったりした結果私がたどりついたのが、Model.get_or_insert()を使った手法。大きなエンティティグループを作る必要がないので、スケーラビリティが良いし、とてもシンプル。実際のコードはこんな感じになる。
class EmailAddress(db.Model):
salt = db.StringProperty(required=True)
@classmethod
def insert_or_fail(cls, email):
salt = str(time.time())+str(random.random())
email = cls.get_or_insert(key_name=email, salt=salt)
return email.salt == salt and email or None
アプリケーション側では、
email = EmailAddress.insert_or_fail("[email protected]")
のように使う。"[email protected]"がまだ登録されていなければ新しく作ったエンティティが返されるし(すでにput()済みのもの)、そうでなければNoneが返される。
この仕組みがちゃんとマルチスレッド環境で動くのは、Model.get_or_insert()自身が内部でトランザクション処理をしており、すでに同じkey_nameのものがデータベース上に存在する場合はそのエンティティを返し(その場合は、与えたプロパティの初期値は無視される)、そうでない場合は指定したプロパティの初期値を持つエンティティをデータベース上にput()した上で返してくれるように作られているからだ。
get_or_insert()を呼ぶ際に、salt=として十分にユニークな文字列を与えておけば(この場合は現在の時間と乱数)、返って来たエンティティのsaltプロパティが与えた文字列と一致しているかどうかを見れば、自分が作ったエンティティなのかすでにもともと存在したエンティティなのかが判別できる。
一つ注意して欲しいのは、この仕組みが使えるのは key_name というプライマリーキーのみであり、他のプロパティには使えない点。そのため、一つのエンティティに二つ以上のユニークなプロパティを持たせたい場合(例えば各ユーザーのメアドとハンドルネームの両方がユニークでなければならない場合)、プライマリーキーにできないプロパティ値を key_name として持つ別の Model を作り、それを使ってユニークな値を保証した上で、メインの Model のプロパティとして使う、ということをしなければならない。
>この仕組みが使えるのは key_name というプライマリーキーのみ
他のpropertyでもFetchできるので、検索結果の有無を判断すれば同じように出来る気がします。Keyの検索はSSTable(メモリー)上で行なわれ、また実際のEntityの検索は行わないので、高速だと思いますが、EntityのFetchもそれなりに高速だと思いますので、新規作成でもEntityを「ハンドルネーム」でFetchして、ユニークかどうかを判定すればいいのではないかと。
それから、ランダムなキー生成やっちゃうと、件数取得で苦労するかもしれません。全件であればstatics apiでなんとかなりますが、個別だとちょっと遅いので、個人的にはカウンタで管理するのがいいのかなと思っています。ユニーク制約も実現できるし。(話がずれていたらすみません)
Posted by: takezaki | 2009.11.10 at 20:32
大変興味を持ちました。
「Model.get_or_insert()自身が内部でトランザクション処理をしており」と書いておられるのは、get_or_insert()の内部で、get と insert がアトミックに実行されるということでしょうか。もしそうではなく、get と insert の間で、他のトランザクションが insert する可能性があるなら、キーの一意性はやはり保証されないことになるように思います。
調べればわかることかもしれませんが、Web検索してもよくわからなかったので、ご教示頂ければ有難いです。
Posted by: Keis | 2009.11.10 at 22:23
追記:
QueryでFetchする場合、トランザクションに参加できないので、注意する必要があります。
Javaですがシーケンス番号をレコードにつける方法を考えてみました。http://blog.virtual-tech.net/2009/11/google-app-engine.html
Posted by: takezaki | 2009.11.17 at 07:57