データストアでのコールバック

注: 他のアプリが 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) {
        Future result = 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 つのコールバックをご覧ください)。この場合、コンパイル エラーが発生するはずです。