Nota: Las devoluciones de llamada no se activan si otra app llama a tu app de App Engine mediante la API remota.
Una devolución de llamada permite ejecutar código en varios puntos del proceso de persistencia. Estas devoluciones de llamada se pueden usar para implementar de forma sencilla una lógica multifuncional para operaciones de registro, muestreo, decoración, auditoría y validación, entre otras. En este momento, la API de Datastore admite devoluciones de llamada que se ejecutan antes y después de las operaciones put()
y delete()
.
PrePut
Cuando registras una devolución de llamada PrePut
con una categoría específica, la devolución de llamada se invoca antes de ubicar cualquier entidad de esa categoría (o, si no se proporciona ninguna categoría, antes de ubicar cualquier entidad):
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 método anotado con PrePut
debe ser un método de instancia que muestre void
, acepte un PutContext
como único argumento, no genere ninguna excepción verificada y pertenezca a una clase con un constructor sin argumentos. Cuando una devolución de llamada PrePut
genera una excepción, no se ejecutan más devoluciones de llamada, y la operación put()
genera la excepción antes de que se emitan las RPC al almacén de datos.
PostPut
Cuando registras una devolución de llamada PostPut
con una categoría específica, esta se invocará después de que se ubique cualquier entidad de esa categoría (o, si no se aclara la categoría, después de ubicar cualquier entidad).
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 método anotado con PostPut
debe ser un método de instancia que muestre void
, acepte un PutContext
como único argumento, no genere ninguna excepción verificada y pertenezca a una clase con un constructor sin argumentos. Cuando una devolución de llamada PostPut
genera una excepción, no se ejecutan más devoluciones de llamada, pero el resultado de la operación put()
no se ve afectado. Ten en cuenta que si la operación put()
falla, no se invocarán las devoluciones de llamada PostPut
en absoluto. Además, las devoluciones de llamada PostPut
que se asocian a las operaciones de transacción put()
no se ejecutarán hasta que la transacción se confirme de forma correcta.
PreDelete
Cuando registras una devolución de llamada PreDelete
con una categoría específica, la devolución de llamada se invoca antes de borrar cualquier entidad de esa categoría (o si no se proporciona ninguna categoría antes de borrar una entidad):
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 método anotado con PreDelete
debe ser un método de instancia que muestre void
, acepte un DeleteContext
como único argumento, no genere ninguna excepción verificada y pertenezca a una clase con un constructor sin argumentos. Cuando una devolución de llamada PreDelete
genera una excepción, no se ejecutan más devoluciones de llamada, y la operación delete()
genera la excepción antes de que se emitan las RPC al almacén de datos.
PostDelete
Cuando registras una devolución de llamada PostDelete
con una categoría específica, esta se invoca después de borrar cualquier entidad de esa categoría (o, si no se proporciona ninguna categoría, después de borrar cualquier entidad):
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 método anotado con PostDelete
debe ser un método de instancia que muestre void
, acepte un DeleteContext
como único argumento, no genere ninguna excepción verificada y pertenezca a una clase con un constructor sin argumentos. Cuando una devolución de llamada PostDelete
genera una excepción, no se ejecutan más devoluciones de llamada, pero el resultado de la operación delete()
no se ve afectado. Ten en cuenta que si la operación delete()
falla, no se invocarán las devoluciones de llamada PostDelete
en absoluto. Además, las devoluciones de llamada PostDelete
que se asocian a las operaciones de transacción delete()
no se ejecutarán hasta que la transacción se confirme de forma correcta.
PreGet
Puedes registrar una devolución de llamada
PreGet
para que se la llame antes de obtener entidades de algunas categorías específicas (o de todas las categorías). Esta función se puede usar, por ejemplo, para interceptar algunas solicitudes get y recuperar los datos desde una caché en lugar de hacerlo desde el almacén de datos.
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
Puedes registrar una devolución de llamada
PreQuery
para que se llame antes de ejecutar consultas sobre categorías específicas (o de todas las categorías). Esta función se puede usar, por ejemplo, para agregar un filtro de igualdad basado en el usuario con el que se accedió.
La consulta se pasa a la devolución de llamada y, si se modifica, puede generar la ejecución de una consulta distinta en su lugar.
Otra opción es que la devolución de llamada genere una excepción sin verificar para prevenir que se ejecute la consulta.
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
Puedes registrar una devolución de llamada
PostLoad
para que se llame después de cargar entidades de categorías específicas (o de todas las categorías). En este caso, “cargar” puede referirse al resultado de una operación get o de una consulta. Esto sirve para decorar entidades cargadas.
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()); } }
Operaciones por lotes
Cuando ejecutas una operación por lotes (una put()
con varias entidades, por ejemplo), las devoluciones de llamada se invocan una vez por entidad. Puedes acceder a todo el lote de objetos si llamas a CallbackContext.getElements()
en el argumento de tu método de devolución de llamada. Esto permite implementar devoluciones de llamada que funcionan en todo el lote en lugar de en un elemento a la vez.
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 necesitas que la devolución de llamada se ejecute una sola vez por lote, usa CallbackContext.getCurrentIndex()
para determinar si el elemento que ves es el primero del lote.
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()); } } }
Operaciones asíncronas
Hay algunas cuestiones importantes que debes saber sobre la interacción de las devoluciones de llamada con las operaciones asíncronas del almacén de datos. Cuando ejecutas put()
o delete()
mediante la API del almacén de datos asíncrono, cualquier devolución de llamada Pre*
que registraste se ejecutará de forma síncrona. Las devoluciones de llamada Post*
también se ejecutarán de forma síncrona, pero no hasta que llames a cualquiera de los métodos Future.get()
para recuperar el resultado de la operación.
Usa devoluciones de llamada con los servicios de App Engine
Las devoluciones de llamada, como cualquier otro código de tu aplicación, tienen acceso al rango completo de servicios de backend de App Engine y puedes definir todas las que quieras en una sola clase.
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(); // ... } }
Errores comunes que debes evitar
Hay una serie de errores comunes que hay que evitar cuando se implementan las devoluciones de llamada.
- No mantengas el estado no estático
- No hagas suposiciones sobre el orden de ejecución de la devolución de llamada
- Implementa solo una devolución de llamada por método
- No olvides recuperar los resultados asíncronos
- Evita los bucles infinitos
No mantengas el estado no estático
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()); } } }
No hagas suposiciones sobre el orden de ejecución de la devolución de llamada
Las devoluciones de llamada Pre*
siempre se ejecutarán antes de las devoluciones de llamada Post*
, pero no es seguro hacer ninguna suposición sobre el orden en que se ejecuta una devolución de llamada Pre*
en relación con otras devoluciones de llamada Pre*
, ni sobre el orden en que se ejecuta una devolución de llamada Post*
en relación con otras devoluciones de llamada 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!"); } } }
Implementa solo una devolución de llamada por método
Si bien una clase puede tener una cantidad ilimitada de métodos de devolución de llamada, se puede asociar un único método a una sola devolución de llamada.
import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PostPut; class MultipleCallbacksOnAMethod { @PrePut @PostPut // Compiler error! void foo(PutContext context) { } }
No olvides recuperar los resultados asíncronos
Las devoluciones de llamada Post*
no se ejecutarán hasta que llames a Future.get()
para recuperar el resultado de la operación. Si olvidas llamar a Future.get()
antes de finalizar el servicio de la solicitud HTTP, las devoluciones de llamada Post*
no se ejecutarán.
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 los bucles infinitos
Si ejecutas operaciones del almacén de datos en tus devoluciones de llamada, ten cuidado de no caer en un bucle 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); } }
Usa devoluciones de llamada con Eclipse
Si desarrollas tu app con Eclipse, debes seguir unos pocos pasos de configuración para usar las devoluciones de llamada en el almacén de datos. Estos son los pasos para Eclipse 3.7. Esperamos que estos pasos sean innecesarios en una versión futura de Google Plugin for Eclipse.
- Abrir el diálogo de propiedades del proyecto (Proyecto > Propiedades)
- Abrir el diálogo de procesamiento de anotaciones (Compilador de Java > Procesamiento de anotaciones)
- Marcar la casilla "Habilitar procesamiento de anotaciones"
- Marcar la casilla "Habilitar procesamiento en el editor"
- Abrir el diálogo de ruta de fábrica (Compilador de Java > Procesador de anotaciones > Ruta de fábrica)
- Hacer clic en "Agregar archivos JAR externos"
- Seleccionar <SDK_ROOT>/lib/impl/appengine-api.jar (donde SDK_ROOT es el directorio de mayor nivel de la instalación de tu SDK)
- Hacer clic en "Aceptar"
Puedes verificar que las devoluciones de llamada estén bien configuradas con la implementación de un método con varias devoluciones de llamada (consulta el fragmento de código Usa solo una devolución de llamada por método). Esto debería generar un error de compilación.