Hatena::ブログ(Diary)

yvsu pron. yas このページをアンテナに追加 RSSフィード

2009-11-11

App Engineのユニーク制限を正しく理解しよう

Google App EngineではRDBMSのようなUnique Indexサポートしていません。ユニーク制限を実現する場合は、トランザクション中でKeyを使ったgetとputを組み合わせる必要があります。


ここでは、email addressがユニークだったらそれを確定してtrueを返し、そうでない場合にはfalseを返すコードを考えます。

最初にトランザクションを使わないコードを見てみましょう。KeyFactory.createKeyの最初に引数は、kindといってテーブル名みたいなものです。

public boolean putUniqueEmailAddress(String value) {
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
    Key key = KeyFactory.createKey("EmailAddress", value);
    try {
        ds.get(key);
        // 既にエントリが存在する
        return false;
    } catch (EntityNotFoundException e) {
        // 見つからなかったのでputする
        Entity entity = new Entity(key);
        ds.put(tx, entity);
        // ユニークな値が確保できた
        return true;
    }
} 


ぱっと見うまくいきそうですね。しかし、getとputの間に他の人が既にデータをputしていた場合でも、エラーになるのではなく上書きしてしまいます。

上書きを避けるために、EmailAddress Entityにユニークになるようなランダムな値を持たせputした後に、getして確認する方法を思いつくかもしれませんが、putからgetの間にまた別の人に更新されているかもしれないので、余り意味はありません。

正解は、トランザクションを使うこと。トランザクションを使う場合には、AppEngineがユニークなのを保証しているので、ランダムな値で確認する必要はありません。

トランザクションを使った正解のコードはこちら。

public boolean putUniqueEmailAddress(String value) {
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
    Key key = KeyFactory.createKey("EmailAddress", value);
    Transaction tx = ds.beginTransaction();
    try {
        ds.get(tx, key);
        tx.rollback();
        // 既にエントリが存在する
        return false;
    } catch (EntityNotFoundException e) {
        // 見つからなかったのでputする
        Entity entity = new Entity(key);
        try {
            ds.put(tx, entity);
            tx.commit();
            // ユニークな値が確保できた
            return true;
        } catch (ConcurrentModificationException e2) {
            // getしてからcommitする間に他の誰かがputしている場合は例外になる
            if (tx.isActive()) {
                tx.rollback();
            }
            return false;
        }
    }
} 


これを応用すれば、KeyはemailAddressなんだけど、twitterのscreenNameのようにユニーク名前も確保したいという要望にも対応することができます。ポイントは、先ほどのkindを決め打ちしていたところを引数で渡せるようにすること。

public boolean putUniqueValue(String uniqueIndexName, String value) {
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
    Key key = KeyFactory.createKey(uniqueIndexName, value);
    Transaction tx = ds.beginTransaction();
    try {
        ds.get(tx, key);
        // 既にエントリが存在する
        return false;
    } catch (EntityNotFoundException e) {
        // 見つからなかったのでputする
        Entity entity = new Entity(key);
        try {
            ds.put(tx, entity);
            tx.commit();
            // ユニークな値が確保できた
            return true;
        } catch (ConcurrentModificationException e2) {
            // getしてからcommitする間に他の誰かがputしている場合は例外になる
            if (tx.isActive()) {
                tx.rollback();
            }
            return false;
        }
    }
} 


使い方はこんな感じ。

if (putUniqueValue("screenName", "higayasuo")) {
    account.setScreenName("higayasuo");
} else {
    throw ...
}


追記:

AppEngineのトランザクションは、EntityGroupのrootのEntityのタイムスタンプによる楽観的排他制御で行われています。詳しくは、下記を参照。

App EngineのEntityGroupを理解しよう - yvsu pron. yas

そして、どのようにしてタイムスタンプで楽観的排他制御が行われているかというと、トランザクション中の最初のget or putの時のrootとなる親のlast committed timestampが取得され、commitの時に、取得したときと同じかどうかで判断されれています。

queryではタイムスタンプが取得されないので、queryで取得したEntityをputしてcommitするとputしてcommitする間の排他制御になるので注意が必要です。

queryは、ancestor queryを除いては、トランザクションに参加できません。


あわせて読みたい

Life is beautiful: Google App Engine入門:Datastore上で「ユニーク制限」を実現する方法

keiskeis 2009/11/11 15:02 いつも拝見させて頂いています。
とても興味深い記事ですね。

>AppEngineのトランザクションは、EntityGroupのrootのEntityのタイムスタンプによる楽観的排他制御で行われています。

これは、「トランザクションを使った正解のコード」で、「getしてからcommitする間に他の誰かがputしている場合は例外になる」理由としてお書きになられているのでしょうか。
そうだとすると疑問が湧くのですが、このコードの最初の getでEntityNotFoundException が投じられた場合、指定された key を持つEntityはまだ存在していないわけです。そうすると、そのEntityに結び付けられた「タイムスタンプ」も存在しないので、楽観的排他制御を適用するすべはないように思います。誤解でしょうか。
あるいは、App Engine では、key を用いてgetしたときにEntity が存在しなければ、その key に結びつけた仮のタイムスタンプを生成してどこかに保持するといった仕掛けがあるのでしょうか。
タイムスタンプによる楽観的排他制御は、更新処理に対してのみ適用できると自分勝手に理解していましたので、興味が湧きました。

higayasuohigayasuo 2009/11/11 15:20 私の試した限りでは、getしたときに存在していない場合でも、仮のtimestampが発行されていて、commitのときにそのtimestampと比較されているような動きをしてます。
timestampそのものは、DatastoreのServerで保持しているんじゃないかと予想してます。
TransactionImplクラスにhandleという変数があるので、それがDatastore Server側で持っているtransactionの状態に対するhandleの気がします。

投稿したコメントは管理者が承認するまで公開されません。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証