Rappels Datastore

Remarque : Les rappels ne sont pas déclenchés si l'application App Engine est appelée par une autre application à l'aide de l'API Remote.

Un rappel permet d'exécuter du code à différents moments du processus de persistance. Vous pouvez utiliser ces rappels pour mettre en œuvre facilement des logiques pluridisciplinaires, telles qu'une journalisation, un échantillonnage, une décoration, des audits et une validation (entre autres). L'API Datastore prend actuellement en charge les rappels qui s'exécutent avant et après les opérations put() et delete().

PrePut

Lorsque vous enregistrez un rappel PrePut en spécifiant un genre, le rappel est appelé avant tout "put" sur une entité de ce genre. Si aucun genre n'est spécifié, le rappel est appelé avant tout "put", toutes entités confondues.

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

Une méthode annotée avec PrePut doit être une méthode d'instance qui renvoie void, accepte un PutContext comme unique argument, ne génère aucune exception vérifiée et appartient à une classe pourvue d'un constructeur sans argument. Lorsqu'un rappel PrePut génère une exception, aucun autre rappel n'est exécuté, et l'opération put() génère l'exception avant tout appel de procédure à distance (RPC) vers le datastore.

PostPut

Lorsque vous enregistrez un rappel PostPut en spécifiant un genre, le rappel est appelé après tout "put" sur une entité de ce genre. Si aucun genre n'est spécifié, le rappel est appelé après tout "put", toutes entités confondues.

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

Une méthode annotée avec PostPut doit être une méthode d'instance qui renvoie void, accepte un PutContext comme unique argument, ne génère aucune exception vérifiée et appartient à une classe pourvue d'un constructeur sans argument. Lorsqu'un rappel PostPut génère une exception, aucun autre rappel n'est exécuté, mais le résultat de l'opération put() n'est pas affecté. Notez que si l'opération put() échoue, les rappels PostPut ne seront pas appelés du tout. De même, les rappels PostPut associés aux opérations put() transactionnelles ne seront exécutés que lorsque la transaction aura été validée.

PreDelete

Lorsque vous enregistrez un rappel PreDelete en spécifiant un genre, le rappel est appelé avant toute suppression d'une entité de ce genre. Si aucun genre n'est spécifié, le rappel est appelé avant toute suppression, toutes entités confondues :

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

Une méthode annotée avec PreDelete doit être une méthode d'instance qui renvoie void, accepte un DeleteContext comme unique argument, ne génère aucune exception vérifiée et appartient à une classe pourvue d'un constructeur sans argument. Lorsqu'un rappel PreDelete génère une exception, aucun autre rappel n'est exécuté, et l'opération delete() génère l'exception avant tout appel de procédure à distance (RPC) vers le datastore.

PostDelete

Lorsque vous enregistrez un rappel PostDelete en spécifiant un genre, le rappel est appelé après toute suppression d'une entité de ce genre. Si aucun genre n'est spécifié, le rappel est appelé après toute suppression, toutes entités confondues :

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

Une méthode annotée avec PostDelete doit être une méthode d'instance qui renvoie void, accepte un DeleteContext comme unique argument, ne génère aucune exception vérifiée et appartient à une classe pourvue d'un constructeur sans argument. Lorsqu'un rappel PostDelete génère une exception, aucun autre rappel n'est exécuté, mais le résultat de l'opération delete() n'est pas affecté. Notez que si l'opération delete() échoue, les rappels PostDelete ne seront pas appelés du tout. De même, les rappels PostDelete associés aux opérations delete() transactionnelles ne seront exécutés que lorsque la transaction aura été validée.

PreGet

Vous pouvez enregistrer un rappel PreGet qui sera appelé avant l'obtention d'entités de genres spécifiques (ou de n'importe quel genre). Vous pouvez utiliser cette option, par exemple, pour intercepter certaines requêtes "get" et récupérer des données à partir d'un cache plutôt que du datastore.

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

Vous pouvez enregistrer un rappel PreQuery qui sera appelé avant l'exécution de requêtes sur des genres spécifiques (ou de n'importe quel genre). Vous pouvez utiliser cette option, par exemple, pour ajouter un filtre d'égalité basé sur l'utilisateur connecté. Le rappel est transmis à la requête. En la modifiant, le rappel peut entraîner l'exécution d'une requête différente. Ou il peut générer une exception non contrôlée pour empêcher l'exécution de la requête.

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

Vous pouvez enregistrer un rappel PostLoad qui sera appelé après le chargement d'entités de genres spécifiques (ou de n'importe quel genre). Ici, le "chargement" peut se rapporter au résultat d'une opération "get" ou d'une requête. Cela s'avère utile pour décorer des entités chargées.

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

Opérations par lots

Lorsque vous exécutez une opération par lot (un put() avec plusieurs entités, par exemple), vos rappels sont appelés une fois par entité. Vous pouvez accéder à l'intégralité du lot d'objets en appelant CallbackContext.getElements() sur l'argument de votre méthode de rappel. Cela vous permet de mettre en œuvre des rappels qui fonctionnement sur le lot entier plutôt que sur un seul élément à la fois.

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

Si vous souhaitez que votre rappel ne s'exécute qu'une seule fois par lot, utilisez CallbackContext.getCurrentIndex() pour déterminer si l'élément courant est le premier élément du lot.

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

Opérations asynchrones

Il existe quelques points importants à savoir sur la manière dont les rappels interagissent avec les opérations de datastore asynchrones. Lorsque vous exécutez une opération put() ou delete() à l'aide de l'API Async Datastore, tous les rappels Pre* que vous avez enregistrés s'exécutent de manière synchrone. Vos rappels Post* s'exécutent également de manière synchrone, mais pas avant que vous n'ayez appelé l'une des méthodes Future.get() pour récupérer le résultat de l'opération.

Utiliser des rappels avec les services App Engine

Les rappels, comme tous les autres codes de l'application, ont accès à la gamme complète des services de backend App Engine, et vous pouvez en définir autant que vous le souhaitez dans une même classe.

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

Erreurs courantes à éviter

Il existe un certain nombre d'erreurs courantes dont vous devez tenir compte lors de la mise en œuvre des rappels.

Ne pas conserver un état non statique

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

Ne pas formuler d'hypothèses sur l'ordre d'exécution des rappels

Les rappels Pre* sont toujours exécutés avant les rappels Post*, mais il est prudent de ne pas faire d'hypothèse sur l'ordre d'exécution d'un rappel Pre* par rapport à un autre rappel Pre*, ni sur l'ordre d'exécution d'un rappel Post* par rapport à un autre rappel 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!");
        }
    }
}

Un rappel par méthode

Même si une classe peut disposer d'un nombre illimité de méthodes de rappel, une méthode ne peut être associée qu'à un seul rappel.

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

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

Ne pas oublier de récupérer les résultats asynchrones

Les rappels Post* ne sont exécutés que lorsque vous appelez Future.get() pour récupérer le résultat de l'opération. Si vous oubliez d'appeler Future.get() avant d'avoir terminé le traitement de la requête HTTP, vos rappels Post* ne sont pas exécutés.

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

Éviter les boucles infinies

Si vous effectuez des opérations de datastore dans vos rappels, veillez à ne pas tomber dans une boucle infinie.

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

Utiliser des rappels avec Eclipse

Si vous développez l'application avec Eclipse, vous devrez effectuer quelques étapes de configuration pour utiliser les rappels de datastore. Ces étapes concernent Eclipse 3.7. Nous espérons que ces étapes ne seront plus nécessaires dans une prochaine version du plugin Google pour Eclipse.

  • Ouvrez la boîte de dialogue "Propriétés" de votre projet (Projet > Propriétés).
  • Ouvrez la boîte de dialogue "Traitement des annotations" (Compilateur Java > Traitement des annotations).
  • Cochez "Enable annotation processing" (Activer le traitement des annotations)
  • Cochez "Enable processing in editor" (Activer le traitement dans l'éditeur)
  • Ouvrez la boîte de dialogue "Factory Path" (Chemin d'usine) (Compilateur Java > Traitement des annotations > Chemin d'usine)
  • Cliquez sur "Add External JARs" (Ajouter des fichiers JAR externes)
  • Sélectionnez <SDK_ROOT>/lib/impl/appengine-api.jar (où SDK_ROOT est le répertoire de niveau supérieur de l'installation du SDK)
  • Cliquez sur "OK"

Vous pouvez vérifier que les rappels sont correctement configurés en mettant en œuvre une méthode avec plusieurs rappels (consultez l'extrait de code sous Un rappel par méthode). Cela devrait générer une erreur de compilateur.