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上で「ユニーク制限」を実現する方法