Retrollamadas de Datastore

Nota: Las retrollamadas no se activan si otra aplicación llama a la aplicación de App Engine mediante la API remota.

Una retrollamada te permite ejecutar código en varios puntos del proceso de persistencia. Puedes usar estas retrollamadas para implementar fácilmente lógica multifuncional, como el registro, el muestreo, la decoración, la auditoría y la validación (entre otras cosas). Actualmente, la API de Datastore admite retrollamadas que se ejecutan antes y después de las operaciones put() y delete().

PrePut

Cuando registras una retrollamada PrePut con un tipo específico, la retrollamada se invoca antes de que se coloque cualquier entidad de ese tipo (o, si no se proporciona ningún tipo, antes de que se coloque 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 devuelva void, acepte un PutContext como único argumento, no genere ninguna excepción comprobada y pertenezca a una clase con un constructor sin argumentos. Cuando una retrollamada de PrePut genera una excepción, no se ejecutan más retrollamadas y la operación de put() genera la excepción antes de que se emita ninguna RPC al almacén de datos.

PostPut

Cuando registras una retrollamada PostPut con un tipo específico, la retrollamada se invoca después de que se haya insertado cualquier entidad de ese tipo (o, si no se proporciona ningún tipo, después de que se haya insertado 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 devuelva void, acepte un PutContext como único argumento, no genere ninguna excepción comprobada y pertenezca a una clase con un constructor sin argumentos. Cuando una retrollamada PostPut genera una excepción, no se ejecutan más retrollamadas, 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á ninguna retrollamada PostPut. Además, las retrollamadas de PostPut asociadas a operaciones transaccionales de put() no se ejecutarán hasta que la transacción se confirme correctamente.

PreDelete

Cuando registras una retrollamada PreDelete con un tipo específico, la retrollamada se invoca antes de que se elimine cualquier entidad de ese tipo (o, si no se proporciona ningún tipo, antes de que se elimine cualquier 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 devuelva void, acepte un DeleteContext como único argumento, no genere ninguna excepción comprobada y pertenezca a una clase con un constructor sin argumentos. Cuando una retrollamada de PreDelete genera una excepción, no se ejecutan más retrollamadas y la operación de delete() genera la excepción antes de que se emitan RPCs al almacén de datos.

PostDelete

Cuando registras una retrollamada PostDelete con un tipo específico, la retrollamada se invoca después de que se elimine cualquier entidad de ese tipo (o, si no se proporciona ningún tipo, después de que se elimine 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 devuelva void, acepte un DeleteContext como único argumento, no genere ninguna excepción comprobada y pertenezca a una clase con un constructor sin argumentos. Cuando una retrollamada PostDelete genera una excepción, no se ejecutan más retrollamadas, 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á ninguna retrollamada PostDelete. Además, las retrollamadas de PostDelete asociadas a operaciones transaccionales de delete() no se ejecutarán hasta que la transacción se confirme correctamente.

PreGet

Puede registrar una retrollamada de PreGet para que se llame antes de obtener entidades de algunos tipos específicos (o de todos los tipos). Por ejemplo, puedes usarlo para interceptar algunas solicitudes GET y obtener los datos de una caché en lugar de hacerlo del 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

Puede registrar una retrollamada PreQuery para que se llame antes de ejecutar consultas de tipos específicos (o de todos los tipos). Por ejemplo, puede usarlo para añadir un filtro de igualdad basado en el usuario que ha iniciado sesión. La retrollamada recibe la consulta. Si la modifica, la retrollamada puede hacer que se ejecute otra consulta. También puede lanzar una excepción no comprobada para evitar 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 retrollamada PostLoad para que se llame después de cargar entidades de tipos específicos (o de todos los tipos). En este caso, "cargando" puede significar el resultado de una obtención o una consulta. Esto resulta útil 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 en lote

Cuando ejecutas una operación por lotes (por ejemplo, una put() con varias entidades), se invoca una retrollamada por cada entidad. Puedes acceder a todo el lote de objetos llamando a CallbackContext.getElements() en el argumento del método de retrollamada. De esta forma, puedes implementar retrollamadas que operen en todo el lote en lugar de en un elemento cada 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 retrollamada solo se ejecute una vez por lote, usa CallbackContext.getCurrentIndex() para determinar si estás viendo el primer elemento 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 algunos aspectos importantes que debes tener en cuenta sobre cómo interactúan las retrollamadas con las operaciones asíncronas del almacén de datos. Cuando ejecutas un put() o un delete() con la API de Datastore asíncrona, cualquier retrollamada Pre* que hayas registrado se ejecutará de forma síncrona. Las retrollamadas de Post* también se ejecutarán de forma síncrona, pero no hasta que llames a alguno de los métodos Future.get() para obtener el resultado de la operación.

Usar retrollamadas con servicios de App Engine

Las retrollamadas, al igual que cualquier otro código de tu aplicación, tienen acceso a toda la gama de servicios de backend de App Engine y puedes definir tantas como 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 habituales que se deben evitar

Hay varios errores habituales que debes tener en cuenta al implementar las retrollamadas.

No mantener 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 las retrollamadas

Las retrollamadas Pre* siempre se ejecutarán antes que las retrollamadas Post*, pero no es seguro hacer suposiciones sobre el orden en el que se ejecuta una retrollamada Pre* en relación con otras retrollamadas Pre*, ni tampoco sobre el orden en el que se ejecuta una retrollamada Post* en relación con otras retrollamadas 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!");
        }
    }
}

Una retrollamada por método

Aunque una clase puede tener un número ilimitado de métodos de retrollamada, un método solo se puede asociar a una retrollamada.

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 obtener los resultados asíncronos

Las retrollamadas de Post* no se ejecutan hasta que llamas a Future.get() para obtener el resultado de la operación. Si olvidas llamar a Future.get() antes de terminar de atender la solicitud HTTP, no se ejecutarán las retrollamadas de Post*.

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.
    }
}

Evitar bucles infinitos

Si realizas operaciones de almacén de datos en tus retrollamadas, 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);
    }
}

Usar retrollamadas con Eclipse

Si desarrollas tu aplicación con Eclipse, tendrás que realizar una serie de pasos de configuración para usar las retrollamadas de Datastore. Estos pasos son para Eclipse 3.7. Esperamos que estos pasos no sean necesarios en una versión futura del complemento de Google para Eclipse.

  • Abre el cuadro de diálogo Propiedades de tu proyecto (Proyecto > Propiedades).
  • Abre el cuadro de diálogo Procesamiento de anotaciones (Compilador de Java > Procesamiento de anotaciones).
  • Marca "Habilitar el procesamiento de anotaciones".
  • Marca "Habilitar el procesamiento en el editor".
  • Abre el cuadro de diálogo Ruta de fábrica (Compilador de Java > Procesamiento de anotaciones > Ruta de fábrica).
  • Haz clic en "Add External JARs" (Añadir JARs externos).
  • Selecciona <SDK_ROOT>/lib/impl/appengine-api.jar (donde SDK_ROOT es el directorio de nivel superior de la instalación del SDK).
  • Haz clic en "Aceptar".

Puedes verificar que las retrollamadas estén configuradas correctamente implementando un método con varias retrollamadas (consulta el fragmento de código de la sección Una retrollamada por método). Esto debería generar un error de compilación.