注: 他のアプリが Remote API を使用して App Engine アプリを呼び出している場合には、コールバックはトリガーされません。
コールバックを使用すると、永続化プロセスのさまざまなタイミングでコードを実行できます。複数の関数にわたるロジック(特にログの記録、サンプリング、装飾、監査、検証など)の実装は、コールバックを使用することで簡単になります。Datastore API では現在、put()
オペレーションと delete()
オペレーションの前後でのコールバックの実行をサポートしています。
PrePut
特定の種類に対して PrePut
コールバックを登録すると、その種類のエンティティの put の前にコールバックが呼び出されます(種類を指定しない場合は、どのエンティティの put の前にも呼び出されます)。
import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PutContext; import java.util.Date; import java.util.logging.Logger; class PrePutCallbacks { static Logger logger = Logger.getLogger("callbacks"); @PrePut(kinds = {"Customer", "Order"}) void log(PutContext context) { logger.fine("Putting " + context.getCurrentElement().getKey()); } @PrePut // Applies to all kinds void updateTimestamp(PutContext context) { context.getCurrentElement().setProperty("last_updated", new Date()); } }
PrePut
と指定するメソッドは、void
を返すインスタンス メソッドである必要があり、唯一の引数として PutContext
を受け取り、チェック例外はスローせず、引数のないコンストラクタを持つクラスに属します。PrePut
コールバックが例外をスローすると、コールバックはそれ以上実行されず、データストアに対して RPC が実行される前に、put()
オペレーションが例外をスローします。
PostPut
特定の種類に対して PostPut
コールバックを登録すると、その種類のエンティティの put の後にコールバックが呼び出されます(種類を指定しない場合は、どのエンティティの put の後にも呼び出されます)。
import com.google.appengine.api.datastore.PostPut; import com.google.appengine.api.datastore.PutContext; import java.util.logging.Logger; class PostPutCallbacks { static Logger logger = Logger.getLogger("logger"); @PostPut(kinds = {"Customer", "Order"}) // Only applies to Customers and Orders void log(PutContext context) { logger.fine("Finished putting " + context.getCurrentElement().getKey()); } @PostPut // Applies to all kinds void collectSample(PutContext context) { Sampler.getSampler().collectSample( "put", context.getCurrentElement().getKey()); } }
PostPut
と指定するメソッドは、void
を返すインスタンス メソッドである必要があり、唯一の引数として PutContext
を受け取り、チェック例外はスローせず、引数のないコンストラクタを持つクラスに属します。PostPut
コールバックが例外をスローすると、コールバックはそれ以上実行されませんが、put()
オペレーションの結果に影響はありません。put()
オペレーションそのものが失敗した場合、PostPut
コールバックはまったく呼び出されません。また、トランザクション put()
オペレーションと関連付けられた PostPut
コールバックは、トランザクションが正常に commit するまで実行されません。
PreDelete
特定の種類に対して PreDelete
コールバックを登録すると、その種類のエンティティが削除される前にコールバックが呼び出されます(種類を指定しない場合は、どのエンティティの削除の前にも呼び出されます)。
import com.google.appengine.api.datastore.DeleteContext; import com.google.appengine.api.datastore.PreDelete; import java.util.logging.Logger; class PreDeleteCallbacks { static Logger logger = Logger.getLogger("logger"); @PreDelete(kinds = {"Customer", "Order"}) void checkAccess(DeleteContext context) { if (!Auth.canDelete(context.getCurrentElement()) { throw new SecurityException(); } } @PreDelete // Applies to all kinds void log(DeleteContext context) { logger.fine("Deleting " + context.getCurrentElement().getKey()); } }
PreDelete
と指定するメソッドは、void
を返すインスタンス メソッドである必要があり、唯一の引数として DeleteContext
を受け取り、チェック例外はスローせず、引数のないコンストラクタを持つクラスに属します。PreDelete
コールバックが例外をスローすると、コールバックはそれ以上実行されず、データストアに対して RPC が実行される前に、delete()
オペレーションが例外をスローします。
PostDelete
特定の種類に対して PostDelete
コールバックを登録すると、その種類のエンティティが削除された後にコールバックが呼び出されます(種類を指定しない場合は、どのエンティティの削除の後にも呼び出されます)。
import com.google.appengine.api.datastore.DeleteContext; import com.google.appengine.api.datastore.PostDelete; import java.util.logging.Logger; class PostDeleteCallbacks { static Logger logger = Logger.getLogger("logger"); @PostDelete(kinds = {"Customer", "Order"}) void log(DeleteContext context) { logger.fine( "Finished deleting " + context.getCurrentElement().getKey()); } @PostDelete // Applies to all kinds void collectSample(DeleteContext context) { Sampler.getSampler().collectSample( "delete", context.getCurrentElement().getKey()); } }
PostDelete
と指定するメソッドは、void
を返すインスタンス メソッドである必要があり、唯一の引数として DeleteContext
を受け取り、チェック例外はスローせず、引数のないコンストラクタを持つクラスに属します。PostDelete
コールバックが例外をスローすると、コールバックはそれ以上実行されませんが、delete()
オペレーションの結果に影響はありません。delete()
オペレーションそのものが失敗した場合、PostDelete
コールバックはまったく呼び出されません。また、トランザクション delete()
オペレーションと関連付けられた PostDelete
コールバックは、トランザクションが正常に commit するまで実行されません。
PreGet
PreGet
コールバックを登録して、特定の種類(またはすべての種類)のエンティティを取得する前に呼び出すようにできます。このコールバックを使って、たとえば、get リクエストの一部をインターセプトして、データストアからではなくキャッシュからデータをフェッチできます。
import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.PreGetContext; import com.google.appengine.api.datastore.PreGet; class PreGetCallbacks { @PreGet(kinds = {"Customer", "Order"}) public void preGet(PreGetContext context) { Entity e = MyCache.get(context.getCurrentElement()); if (e != null) { context.setResultForCurrentElement(e); } // if not in cache, don't set result; let the normal datastore-fetch happen } }
PreQuery
PreQuery
コールバックを登録して、特定の種類(またはすべての種類)のクエリを実行する前に呼び出すようにできます。このコールバックを使って、たとえば、ログインしているユーザーに応じた等式フィルタを追加できます。コールバックにクエリが渡されたら、クエリを変更することで、コールバックに異なるクエリを実行させることができます。または、コールバックが非チェック例外をスローして、クエリの実行を阻止することもできます。
import com.google.appengine.api.datastore.PreQueryContext; import com.google.appengine.api.datastore.PreQuery; import com.google.appengine.api.datastore.Query; import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; class PreQueryCallbacks { @PreQuery(kinds = {"Customer"}) // Queries should only return data owned by the logged in user. public void preQuery(PreQueryContext context) { UserService users = UserServiceFactory.getUserService(); context.getCurrentElement().setFilter( new FilterPredicate("owner", Query.FilterOperator.EQUAL, users.getCurrentUser())); } }
PostLoad
PostLoad
コールバックを登録して、特定の種類(またはすべての種類)のエンティティの読み込み後に呼び出すようにできます。ここで「読み込み」とは、取得の結果でも、クエリの結果でもかまいません。このコールバックは、読み込まれたエンティティを装飾する場合に役立ちます。
import com.google.appengine.api.datastore.PostLoadContext; import com.google.appengine.api.datastore.PostLoad; class PostLoadCallbacks { @PostLoad(kinds = {"Order"}) public void postLoad(PostLoadContext context) { context.getCurrentElement().setProperty("read_timestamp", System.currentTimeMillis()); } }
バッチ オペレーション
バッチ オペレーション(たとえば、複数のエンティティの put()
)を実行する際、各エンティティに対してコールバックが 1 回ずつ呼び出されます。バッチ オペレーションのオブジェクト全体にアクセスするには、コールバック メソッドへの引数に対して CallbackContext.getElements()
を呼び出します。これにより、対象の要素を 1 つずつ処理する代わりに、バッチ全体を処理するコールバックを実装できます。
import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PutContext; class Validation { @PrePut(kinds = "TicketOrder") void checkBatchSize(PutContext context) { if (context.getElements().size() > 5) { throw new IllegalArgumentException( "Cannot purchase more than 5 tickets at once."); } } }
バッチ オペレーションに対して 1 回だけコールバックを実行する必要がある場合は、CallbackContext.getCurrentIndex()
を使用して、バッチ オペレーションの最初の要素かどうかを判断します。
import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PutContext; import java.util.logging.Logger; class Validation { static Logger logger = Logger.getLogger("logger"); @PrePut void log(PutContext context) { if (context.getCurrentIndex() == 0) { logger.fine("Putting batch of size " + getElements().size()); } } }
非同期オペレーション
データストアへの非同期オペレーションとコールバックとのやり取りについて、注意の必要な重要な点がいくつかあります。put()
や delete()
を Async Datastore API を使用して実行する際、登録した Pre*
コールバックは同期して実行されます。Post*
コールバックも同様に同期して実行されますが、オペレーションの結果を取得する Future.get()
メソッドのいずれかが呼び出されるまでは実行されません。
App Engine サービスでのコールバックの使用
アプリケーションの他のコードと同様に、コールバックからは、App Engine バックエンド サービスのすべてにアクセスすることができます。また、1 つのクラスに定義できるコールバックの数に制限はありません。
import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.DeleteContext; import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PostPut; import com.google.appengine.api.datastore.PostDelete; import com.google.appengine.api.datastore.PutContext; import com.google.appengine.api.memcache.MemcacheService; import com.google.appengine.api.memcache.MemcacheServiceFactory; import com.google.appengine.api.taskqueue.Queue; import com.google.appengine.api.taskqueue.QueueFactory; import com.google.appengine.api.urlfetch.URLFetchService; import com.google.appengine.api.urlfetch.URLFetchServiceFactory; class ManyCallbacks { @PrePut(kinds = {"kind1", "kind2"}) void foo(PutContext context) { MemcacheService ms = MemcacheServiceFactory.getMemcacheService(); // ... } @PrePut void bar(PutContext context) { DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); // ... } @PostPut(kinds = {"kind1", "kind2"}) void baz(PutContext context) { Queue q = QueueFactory.getDefaultQueue(); // ... } @PostDelete(kinds = {"kind2"}) void yam(DeleteContext context) { URLFetchService us = URLFetchServiceFactory.getURLFetchService(); // ... } }
一般的なエラーの回避
コールバックの実装時に、よく見られるエラーに対する注意点がいくつかあります。
静的ではない状態は維持しない
import java.util.logging.Logger; import com.google.appengine.api.datastore.PrePut; class MaintainsNonStaticState { static Logger logger = Logger.getLogger("logger"); // ERROR! // should be static to avoid assumptions about lifecycle of the instance boolean alreadyLogged = false; @PrePut void log(PutContext context) { if (!alreadyLogged) { alreadyLogged = true; logger.fine( "Finished deleting " + context.getCurrentElement().getKey()); } } }
コールバックの実行順序は仮定しない
Pre*
コールバックは常に Post*
コールバックより前に実行されますが、ある Pre*
コールバックが他の Pre*
コールバックより先または後に実行されると仮定することはできません。同様に、ある Post*
コールバックが他の Post*
コールバックより先または後に実行されると仮定することもできません。
import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.PrePut; import java.util.HashSet; import java.util.Set; class MakesAssumptionsAboutOrderOfCallbackExecution { static Set<Key> paymentKeys = new HashSet<Key>(); @PrePut(kinds = "Order") void prePutOrder(PutContext context) { Entity order = context.getCurrentElement(); paymentKeys.addAll((Collection<Key>) order.getProperty("payment_keys")); } @PrePut(kinds = "Payment") void prePutPayment(PutContext context) { // ERROR! // Not safe to assume prePutOrder() has already run! if (!paymentKeys.contains(context.getCurrentElement().getKey()) { throw new IllegalArgumentException("Unattached payment!"); } } }
1 つのメソッドに 1 つのコールバック
1 つのクラス内のコールバック メソッドの数に制限はありませんが、1 つのメソッドに関連付けられるコールバックは 1 つだけです。
import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PostPut; class MultipleCallbacksOnAMethod { @PrePut @PostPut // Compiler error! void foo(PutContext context) { } }
非同期の結果の取得を忘れない
Post*
コールバックは、Future.get()
を呼び出してオペレーションの結果を取得するまで実行されません。HTTP リクエストの処理を終える前に、Future.get()
を呼び出すのを忘れると、Post*
コールバックは実行されなくなります。
import com.google.appengine.api.datastore.AsyncDatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.PostPut; import com.google.appengine.api.datastore.PutContext; import java.util.concurrent.Future; class IgnoresAsyncResult { AsyncDatastoreService ds = DatastoreServiceFactory.getAsyncDatastoreService(); @PostPut void collectSample(PutContext context) { Sampler.getSampler().collectSample( "put", context.getCurrentElement().getKey()); } void addOrder(Entity order) { Futureresult = ds.put(order); // ERROR! Never calls result.get() so collectSample() will not run. } }
無限ループを避ける
コールバックでデータストア オペレーションを行う場合、無限ループに陥らないよう注意してください。
import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PutContext; class InfiniteLoop { DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); @PrePut void audit(PutContext context) { Entity original = ds.get(context.getCurrentElement().getKey()); Entity auditEntity = new Entity(original.getKind() + "_audit"); auditEntity.setPropertiesFrom(original); // INFINITE LOOP! // Restricting the callback to specific kinds would solve this. ds.put(auditEntity); } }
Eclipse でのコールバックの使用
Eclipse を使用してアプリケーションを開発している場合、データストア コールバックを使用するには、数ステップの構成が必要です。下記の手順は Eclipse 3.7 の場合です。Eclipse 用 Google プラグインの将来のリリースでは、この手順を不要にする予定です。
- プロジェクトの [Properties] ダイアログを開きます([Project] > [Properties])
- [Annotation Processing] ダイアログを開きます([Java Compiler] > [Annotation Processing])
- [Enable annotation processing] チェックボックスをオンにします
- [Enable processing in editor] チェックボックスをオンにします
- [Factory Path] ダイアログを開きます([Java Compiler] > [Annotation Processing] > [Factory Path])
- [Add External JARs] をクリックします
- <SDK_ROOT>/lib/impl/appengine-api.jar を選択します(ここで SDK_ROOT は、SDK をインストールしたトップ レベルのディレクトリです)
- [OK] をクリックします
複数のコールバックを 1 つのメソッドに実装すると、コールバックが正しく構成されているかどうかを確認できます(1 つのメソッドに 1 つのコールバックをご覧ください)。この場合、コンパイル エラーが発生するはずです。