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
- Ne pas formuler d'hypothèses sur l'ordre d'exécution des rappels
- Un rappel par méthode
- Ne pas oublier de récupérer les résultats asynchrones
- Éviter les boucles infinies
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) { Futureresult = 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.