Devolución de llamadas de Datastore

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

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) {
        Future result = 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.