Datenspeicher-Callbacks

Hinweis: Callbacks werden nicht ausgelöst, wenn die App Engine-Anwendung von einer anderen Anwendung über die Remote API aufgerufen wird.

Mit einem Rückruf können Sie an verschiedenen Punkten im Persistenzprozess Code ausführen. Sie können diese Rückrufe verwenden, um eine funktionsübergreifende Logik wie beispielsweise Protokollierung, Stichproben, Dekoration, Auditing und Validierung einfach zu implementieren. Die Datastore API unterstützt derzeit Rückrufe, die vor und nach den Vorgängen put() und delete() ausgeführt werden.

PrePut

Wenn Sie einen PrePut -Rückruf mit einer bestimmten Art registrieren, wird der Rückruf aufgerufen, bevor eine Entität dieser Art (oder, falls keine Art angegeben wird, bevor eine Entität bereitgestellt wird):

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());
    }
}

Eine mit PrePut annotierte Methode muss eine Instanzmethode sein, die void zurückgibt, ein PutContext als einziges Argument akzeptiert, keine geprüften Ausnahmen auslöst und zu einer Klasse gehört mit einem argumentlosen Konstruktor. Wenn ein PrePut - Callback eine Ausnahme auslöst, werden keine weiteren Callbacks ausgeführt und der put() - Vorgang löst die Ausnahme aus, bevor RPCs an den Datenspeicher ausgegeben werden.

PostPut

Wenn Sie einen PostPut -Rückruf mit einer bestimmten Art registrieren, wird der Rückruf aufgerufen, nachdem eine Entität dieser Art (oder, falls keine Art angegeben wurde, nach einer beliebigen Entität) bereitgestellt wird.

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());
    }
}

Eine mit PostPut annotierte Methode muss eine Instanzmethode sein, die void zurückgibt, ein PutContext als einziges Argument akzeptiert, keine geprüften Ausnahmen auslöst und zu einer Klasse gehört mit einem argumentlosen Konstruktor. Wenn ein PostPut - Callback eine Ausnahme auslöst, werden keine weiteren Callbacks ausgeführt, aber das Ergebnis des put() - Vorgangs ist davon nicht betroffen. Wenn der Vorgang put() fehlschlägt, werden PostPut Callbacks überhaupt nicht aufgerufen. Darüber hinaus werden PostPut Callbacks, die transaktionalen put() Operationen zugeordnet sind, erst ausgeführt, wenn die Transaktion erfolgreich committen konnte.

PreDelete

Wenn Sie einen PreDelete -Rückruf mit einer bestimmten Art registrieren, wird der Rückruf aufgerufen, bevor eine Entität dieser Art gelöscht wird (oder, falls keine Art angegeben wird, bevor eine Entität gelöscht wird):

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());
    }
}

Eine mit PreDelete annotierte Methode muss eine Instanzmethode sein, die void zurückgibt, ein DeleteContext als einziges Argument akzeptiert, keine geprüften Ausnahmen auslöst und zu einer Klasse gehört mit einem argumentlosen Konstruktor. Wenn ein PreDelete - Callback eine Ausnahme auslöst, werden keine weiteren Callbacks ausgeführt und der delete() - Vorgang löst die Ausnahme aus, bevor RPCs an den Datenspeicher ausgegeben werden.

PostDelete

Wenn Sie einen PostDelete -Rückruf mit einer bestimmten Art registrieren, wird der Rückruf aufgerufen, nachdem eine Entität dieser Art gelöscht wurde (oder falls keine Entität angegeben wurde, nachdem eine Entität gelöscht wurde):

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());
    }
}

Eine mit PostDelete annotierte Methode muss eine Instanzmethode sein, die void zurückgibt, ein DeleteContext als einziges Argument akzeptiert, keine geprüften Ausnahmen auslöst und zu einer Klasse gehört mit einem argumentlosen Konstruktor. Wenn ein PostDelete - Callback eine Ausnahme auslöst, werden keine weiteren Callbacks ausgeführt, aber das Ergebnis des delete() - Vorgangs ist davon nicht betroffen. Wenn der Vorgang delete() fehlschlägt, werden PostDelete Callbacks überhaupt nicht aufgerufen. Darüber hinaus werden PostDelete Callbacks, die transaktionalen delete() Operationen zugeordnet sind, erst ausgeführt, wenn die Transaktion erfolgreich committen konnte.

PreGet

Sie können einen PreGet Callback registrieren, der aufgerufen werden soll, bevor Entitäten bestimmter Arten (oder aller Arten) abgerufen werden. Dies ist beispielsweise nützlich, um GET-Anfragen abzufangen und die Daten aus einem Cache statt aus dem Datenspeicher abzurufen.

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

Sie können einen PreQuery - Callback registrieren, der aufgerufen werden soll, bevor Abfragen für bestimmte Arten (oder alle Arten) ausgeführt werden. Dies ist beispielsweise nützlich, um einen Gleichheitsfilter basierend auf dem angemeldeten Nutzer hinzuzufügen. Die Abfrage wird an den Rückruf übergeben. Durch eine Änderung der Abfrage kann über den Rückruf stattdessen eine andere Abfrage ausgeführt werden. Oder der Rückruf kann eine ungeprüfte Ausnahme auslösen, um zu verhindern, dass die Abfrage ausgeführt wird.

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

Sie können einen PostLoad - Callback registrieren, der nach dem Laden von Entitäten bestimmter Arten (oder aller Arten) aufgerufen wird. Hier bedeutet "Laden" ggf. das Ergebnis einer GET-Anfrage oder einer Abfrage. Dies ist beim Dekorieren geladener Entitäten von Vorteil.

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());
   }
}

Batchvorgänge

Wenn Sie einen Batchvorgang ausführen (z. B. put() mit mehreren Entitäten), werden Ihre Rückrufe einmal pro Entität aufgerufen. Sie können auf den gesamten Objekt-Batch zugreifen, indem Sie CallbackContext.getElements() für das Argument Ihrer Rückrufmethode aufrufen. Auf diese Weise können Sie Rückrufe implementieren, die für den gesamten Batch und nicht nur für jeweils ein Element gelten.

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.");
        }
    }
}

Wenn Ihr Rückruf nur einmal pro Batch ausgeführt werden soll, verwenden Sie CallbackContext.getCurrentIndex(), um festzustellen, ob Sie das erste Element des Batches betrachten.

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());
        }
    }
}

Asynchrone Vorgänge

Beachten Sie die folgenden wichtigen Hinweise dazu, wie Rückrufe mit asynchronen Datenspeichervorgängen interagieren. Wenn Sie ein ausführen, put() oder a delete() mit der asynchronen Datenspeicher-API , any Pre* Rückrufe, die Sie registriert haben, werden synchron ausgeführt. Ihre Post* - Rückrufe werden ebenfalls synchron ausgeführt, jedoch erst, wenn Sie eine der Future.get() -Methoden aufrufen, um das Ergebnis des Vorgangs abzurufen.

Rückrufe mit App Engine-Diensten verwenden

Rückrufe haben wie jeder andere Code in Ihrer Anwendung Zugriff auf die gesamte Palette der Back-End-Dienste von App Engine. Sie können außerdem beliebig viele Rückrufe in einer einzigen Klasse definieren.

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();
        // ...
    }
}

Häufig auftretende Fehler vermeiden

Beim Implementieren von Rückrufen gibt es eine Reihe häufig auftretender Fehler, die Sie vermeiden sollten.

Nicht-statischen Zustand nicht beibehalten

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());
        }
    }
}

Ausführungsreihenfolge von Rückrufen nicht voraussetzen

Pre* - Callbacks werden immer vor Post* - Callbacks ausgeführt. Es ist jedoch nicht sicher, Annahmen über die Reihenfolge zu treffen, in der ein Pre* - Callback relativ zu anderen Pre* - Callbacks ausgeführt wird machen Sie Annahmen über die Reihenfolge, in der ein Post* - Callback relativ zu anderen Post* - Callbacks ausgeführt wird.

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!");
        }
    }
}

Ein Rückruf pro Methode

Eine Klasse kann zwar eine unbegrenzte Anzahl von Rückrufmethoden haben, trotzdem kann eine einzelne Methode nur einem einzelnen Rückruf zugeordnet werden.

import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PostPut;

class MultipleCallbacksOnAMethod {
    @PrePut
    @PostPut // Compiler error!
    void foo(PutContext context) { }
}

Abrufen asynchroner Ergebnisse nicht vergessen

Post* - Rückrufe werden erst ausgeführt, wenn Sie Future.get() aufrufen, um das Ergebnis des Vorgangs abzurufen. Wenn Sie vergessen, Future.get() aufzurufen, bevor Sie die HTTP-Anfrage bearbeitet haben, werden Ihre Post* - Rückrufe nicht ausgeführt.

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.
    }
}

Endlosschleifen vermeiden

Wenn Sie in Ihren Rückrufen Datenspeichervorgänge durchführen, achten Sie darauf, nicht in eine Endlosschleife zu geraten.

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);
    }
}

Rückrufe mit Eclipse verwenden

Wenn Sie Ihre Anwendung mit Eclipse entwickeln, müssen Sie ein paar Konfigurationsschritte durchführen, um Datenspeicherrückrufe zu verwenden. Diese Schritte gelten für Eclipse 3.7. Diese Schritte werden in einer zukünftigen Version des Google-Plug-ins für Eclipse wahrscheinlich nicht mehr erforderlich sein.

  • Öffnen Sie für Ihr Projekt das Dialogfeld "Eigenschaften" (Projekt > Eigenschaften).
  • Öffnen Sie das Dialogfeld "Annotationsverarbeitung" (Java-Compiler > Annotationsverarbeitung).
  • Klicken Sie auf das Kästchen "Annotationsverarbeitung aktivieren".
  • Klicken Sie auf das Kästchen "Verarbeitung im Editor aktivieren".
  • Öffnen Sie das Dialogfeld "Factory-Pfad" (Java-Compiler > Annotationsverarbeitung > Factory-Pfad)
  • Klicken Sie auf "Externe JARs hinzufügen".
  • Wählen Sie <SDK_ROOT>/lib/impl/appengine-api.jar aus (wobei SDK_ROOT das oberste Verzeichnis Ihrer SDK-Installation ist).
  • Klicken Sie auf "OK".

Sie können überprüfen, ob Rückrufe richtig konfiguriert sind, indem Sie eine Methode mit mehreren Rückrufen implementieren (siehe Code-Snippet unter Ein Rückruf pro Methode). Dies sollte einen Compilerfehler erzeugen.