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 avec un genre spécifique, le rappel est appelé avant qu'une entité de ce genre ne soit placée (ou, si aucun genre n'est fourni, avant qu'une entité ne soit placée) :

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 contexte PutContext comme seul argument, ne génère aucune exception vérifiée et appartient à une classe comportant 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 que des appels de procédure à distance ne soient émis vers le datastore.

PostPut

Lorsque vous enregistrez un rappel PostPut avec un genre spécifique, le rappel est appelé après qu'une entité de ce genre est placée (ou, si aucun genre n'est fourni, après qu'une entité est placée) :

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 contexte PutContext comme seul argument, ne génère aucune exception vérifiée et appartient à une classe comportant un constructeur sans argument. Lorsqu'un rappel PostPut génère une exception, aucun autre rappel n'est exécuté et le résultat de l'opération put() n'est pas affecté. Notez que si l'opération put() elle-même échoue, les rappels PostPut ne seront pas appelés du tout. De plus, les rappels PostPut associés à des opérations put() transactionnelles ne seront exécutés que lorsque la transaction aura effectué un commit.

PreDelete

Lorsque vous enregistrez un rappel PreDelete avec un genre spécifique, le rappel est appelé avant qu'une entité de ce genre ne soit supprimée (ou, si aucun genre n'est fourni, avant qu'une entité ne soit supprimée) :

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 contexte DeleteContext comme seul argument, ne génère aucune exception vérifiée et appartient à une classe comportant 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 que des appels de procédure à distance ne soient émis vers le datastore.

PostDelete

Lorsque vous enregistrez un rappel PostDelete avec un genre spécifique, le rappel est appelé après qu'une entité de ce genre est supprimée (ou, si aucun genre n'est fourni, après qu'une entité est supprimée) :

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 contexte DeleteContext comme seul argument, ne génère aucune exception vérifiée et appartient à une classe comportant un constructeur sans argument. Lorsqu'un rappel PostDelete génère une exception, aucun autre rappel n'est exécuté et le résultat de l'opération delete() n'est pas affecté. Notez que si l'opération delete() elle-même échoue, les rappels PostDelete ne seront pas appelés du tout. De plus, les rappels PostDelete associés à des opérations delete() transactionnelles ne seront exécutés que lorsque la transaction a effectué un commit.

PreGet

Vous pouvez enregistrer un rappel PreGet avant d'obtenir des entités de genres spécifiques (ou de tous genres). 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 à appeler avant d'exécuter des requêtes de genres spécifiques (ou de tous genres). 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 à appeler après le chargement d'entités de genres spécifiques (ou de tous genres). 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 fois par lot, utilisez CallbackContext.getCurrentIndex() pour déterminer si vous observez 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 n'est pas prudent de formuler des hypothèses sur l'ordre dans lequel un rappel Pre* s'exécute par rapport à d'autres rappels Pre*, ni sur l'ordre dans lequel un rappel Post* s'exécute par rapport à d'autres rappels 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.

Cette page vous a-t-elle été utile ? Évaluez-la :

Envoyer des commentaires concernant…

Environnement standard App Engine pour Java 8