Nota: i callback non vengono attivati se l'app App Engine viene chiamata da un'altra app che utilizza l'API Remote.
Un callback ti consente di eseguire codice in vari punti del processo di persistenza. Puoi utilizzare questi callback per implementare facilmente una logica cross-funzionale come il logging, il campionamento, la decorazione, il controllo e la convalida (tra le altre cose). L'API Datastore supporta attualmente i callback che vengono eseguiti prima e dopo le operazioni put()
e delete()
.
PrePut
Quando registri un callback PrePut
con un tipo specifico, il callback viene richiamato prima che venga inserita qualsiasi entità di quel tipo (o, se non viene fornito alcun tipo, prima dell'inserimento di qualsiasi entità):
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()); } }
Un metodo annotato con PrePut
deve essere un metodo di istanza che restituisce void
, accetta PutContext
come unico argomento, non genera eccezioni selezionate e appartiene a una classe con un costruttore no-arg. Quando un callback PrePut
genera un'eccezione, non vengono eseguiti ulteriori callback e l'operazione put()
genera l'eccezione prima che vengano inviate RPC al datastore.
PostPut
Quando registri un callback PostPut
con un tipo specifico, il callback verrà invocato dopo l'inserimento di qualsiasi entità di quel tipo (o, se non viene fornito alcun tipo, dopo l'inserimento di qualsiasi entità).
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()); } }
Un metodo annotato con PostPut
deve essere un metodo di istanza che restituisce void
, accetta PutContext
come unico argomento, non genera eccezioni selezionate e appartiene a una classe con un costruttore no-arg. Quando un callback PostPut
genera un'eccezione, non vengono eseguiti ulteriori callback, ma il risultato dell'operazione put()
non viene modificato. Tieni presente che se l'operazione put()
non riesce, i callback PostPut
non verranno richiamati. Inoltre, i callback PostPut
associati alle operazioni put()
transazionali non verranno eseguiti finché la transazione non viene eseguita correttamente.
PreDelete
Quando registri un callback PreDelete
con un tipo specifico, il callback verrà invocato prima che venga eliminata qualsiasi entità di quel tipo (o, se non viene fornito alcun tipo, prima che venga eliminata qualsiasi entità):
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()); } }
Un metodo annotato con PreDelete
deve essere un metodo di istanza che restituisce void
, accetta DeleteContext
come unico argomento, non genera eccezioni selezionate e appartiene a una classe con un costruttore no-arg. Quando un callback PreDelete
genera un'eccezione, non vengono eseguiti ulteriori callback e l'operazione delete()
genera l'eccezione prima che qualsiasi RPC venga inviata al datastore.
PostDelete
Quando registri un callout PostDelete
con un tipo specifico, il callout verrà invocato dopo l'eliminazione di qualsiasi entità di quel tipo (o, se non viene fornito alcun tipo, dopo l'eliminazione di qualsiasi entità):
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()); } }
Un metodo annotato con PostDelete
deve essere un metodo di istanza che restituisce void
, accetta DeleteContext
come unico argomento, non genera eccezioni selezionate e appartiene a una classe con un costruttore no-arg. Quando un callback PostDelete
genera un'eccezione, non vengono eseguiti ulteriori callback, ma il risultato dell'operazione delete()
non viene modificato. Tieni presente che se l'operazione delete()
non riesce, i callback PostDelete
non verranno richiamati. Inoltre, i callback PostDelete
associati alle operazioni delete()
transazionali non verranno eseguiti finché la transazione non viene eseguita correttamente.
PreGet
Puoi registrare un callback
PreGet
da chiamare prima di ottenere entità di alcuni
tipi specifici (o di tutti i tipi). Potresti utilizzarlo, ad esempio, per
intercettare alcune richieste GET e recuperare i dati da una cache anziché
dal 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
Puoi registrare un
PreQuery
da chiamare prima di eseguire query
tipi specifici (o di tutti i tipi). Ad esempio, puoi utilizzarlo per
aggiungere un filtro di uguaglianza in base all'utente che ha eseguito l'accesso.
Il callback viene passato la query; modificandolo,
il callback può causare l'esecuzione di una query diversa.
In alternativa, il callback può lanciare un'eccezione non controllata per impedire l'esecuzione della query.
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
Puoi registrare un callback
PostLoad
da chiamare dopo il caricamento di entità di tipi specifici (o di tutti i tipi). Qui, "caricamento in corso" potrebbe indicare il risultato
di un comando get o di una query. Ciò è utile per decorare le entità caricate.
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()); } }
Operazioni collettive
Quando esegui un'operazione in batch (ad esempio un put()
con più entità), i callback vengono richiamati una volta per entità. Puoi accedere all'intero batch di oggetti chiamando CallbackContext.getElements()
sull'argomento del tuo metodo di callback. In questo modo puoi implementare i callback che operano sull'intero batch invece che su un elemento alla volta.
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."); } } }
Se vuoi che il tuo callback venga eseguito una sola volta per batch, utilizza CallbackContext.getCurrentIndex()
per determinare se stai esaminando il primo elemento del batch.
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()); } } }
Operazioni asincrone
Esistono alcune informazioni importanti da conoscere sul modo in cui i callback interagiscono con le operazioni del datastore asincrone. Quando esegui un put()
o un delete()
utilizzando l'API di archiviazione dati asincrona, tutti i Pre*
callback che hai registrato verranno eseguiti in modo sincrono. I callback Post*
verranno eseguiti anche in modalità sincrona, ma non finché non richiamerai uno dei metodi Future.get()
per recuperare il risultato dell'operazione.
Utilizzare i rilanci con i servizi App Engine
I callback, come qualsiasi altro codice nella tua applicazione, hanno accesso all'intera gamma di servizi di backend di App Engine e puoi definirne quanti vuoi in un'unica 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(); // ... } }
Errori comuni da evitare
Esistono diversi errori comuni da tenere presente durante l'implementazione dei callback.
- Non mantenere lo stato non statico
- Non fare ipotesi in merito all'ordine di esecuzione del callback
- Un solo callback per metodo
- Non dimenticare di recuperare i risultati asinc
- Evitare i loop infiniti
Non mantenere uno stato non statico
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()); } } }
Non fare supposizioni sull'ordine di esecuzione dei callback
I callback Pre*
verranno sempre eseguiti prima dei callback Post*
, ma non è sicuro fare alcuna ipotesi sull'ordine in cui viene eseguito un callback Pre*
rispetto ad altri callback Pre*
, né fare ipotesi sull'ordine in cui un callback Post*
viene eseguito rispetto ad altri callback 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 callback per metodo
Anche se una classe può avere un numero illimitato di metodi di callback, un singolo metodo può essere associato a un solo callback.
import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PostPut; class MultipleCallbacksOnAMethod { @PrePut @PostPut // Compiler error! void foo(PutContext context) { } }
Non dimenticare di recuperare i risultati asincroni
I Callback Post*
non vengono eseguiti finché non chiami Future.get()
per recuperare il risultato dell'operazione. Se dimentichi di chiamare Future.get()
prima di terminare la gestione della richiesta HTTP, i callback Post*
non verranno eseguiti.
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. } }
Evita i loop infiniti
Se esegui operazioni sul datastore nei callback, fai attenzione a non cadere in un loop infinito.
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); } }
Utilizzare i Callback con Eclipse
Se stai sviluppando la tua app con Eclipse, devi eseguire un numero ridotto di passaggi di configurazione per utilizzare i callback del datastore. Questi passaggi si riferiscono a Eclipse 3.7. Questi passaggi non saranno più necessari in una versione futura del Google Plugin For Eclipse.
- Apri la finestra di dialogo Proprietà del progetto (Progetto > Proprietà).
- Apri la finestra di dialogo Elaborazione delle annotazioni (Compilatore Java > Elaborazione delle annotazioni)
- Seleziona "Attiva l'elaborazione delle annotazioni"
- Seleziona "Abilita elaborazione nell'editor"
- Apri la finestra di dialogo Percorso in fabbrica (Compilatore Java > Elaborazione delle annotazioni > Percorso in fabbrica)
- Fai clic su "Aggiungi JAR esterni"
- Seleziona <SDK_ROOT>/lib/impl/appengine-api.jar (dove SDK_ROOT è la directory di primo livello dell'installazione dell'SDK)
- Fai clic su "OK"
Per verificare che i callback siano configurati correttamente, implementa un metodo con più callback (vedi lo snippet di codice in Un callback per metodo). Dovrebbe generare un errore del compilatore.