Callback di Datastore

Nota: i callback non vengono attivati se l'app App Engine viene chiamata da un'altra app utilizzando l'API Remote.

Un callback consente di eseguire il codice in vari punti del processo di persistenza. Puoi utilizzare questi callback per implementare facilmente logiche interfunzionali come logging, campionamento, decorazione, controllo e convalida, tra le altre cose. L'API Datastore attualmente supporta 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 di inserire qualsiasi entità di quel tipo (o, se non viene specificato alcun tipo, prima di inserire 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 qualsiasi RPC venga inviata al datastore.

PostPut

Quando registri un callback PostPut con un tipo specifico, il callback viene richiamato dopo che è stata inserita qualsiasi entità di quel tipo (o, se non viene specificato alcun tipo, dopo che è stata inserita un'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 va a buon fine, i callback PostPut non verranno attivati. Inoltre, i callback PostPut associati a operazioni put() transazionali non verranno eseguiti finché la transazione non viene confermata correttamente.

PreDelete

Quando registri un callback PreDelete con un tipo specifico, il callback viene richiamato prima che venga eliminata qualsiasi entità di quel tipo (o, se non viene specificato 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 vengano inviate RPC al datastore.

PostDelete

Quando registri un callback PostDelete con un tipo specifico, il callback viene richiamato dopo l'eliminazione di qualsiasi entità di quel tipo (o, se non viene specificato alcun tipo, dopo l'eliminazione di un'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 va a buon fine, i callback PostDelete non verranno attivati. Inoltre, i callback PostDelete associati a operazioni delete() transazionali non verranno eseguiti finché la transazione non viene confermata correttamente.

PreGet

Puoi registrare un callback PreGet da chiamare prima di ottenere entità di alcuni tipi specifici (o di tutti i tipi). Puoi 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 callback PreQuery da chiamare prima di eseguire query per tipi specifici (o tutti i tipi). Puoi utilizzarlo, ad esempio, per aggiungere un filtro di uguaglianza in base all'utente che ha eseguito l'accesso. Il callback viene passato alla query; modificandola, il callback può causare l'esecuzione di una query diversa. In alternativa, il callback può generare un'eccezione deselezionata 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 aver caricato entità di tipi specifici (o di tutti i tipi). In questo caso, "caricamento" potrebbe significare il risultato di un recupero o di una query. È 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 in gruppo

Quando esegui un'operazione batch (ad esempio un put() con più entità), i callback vengono richiamati una volta per entità. Puoi accedere all'intero gruppo di oggetti chiamando CallbackContext.getElements() sull'argomento del tuo metodo di callback. Ciò ti consente di implementare 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 callback venga eseguito una sola volta per batch, utilizza CallbackContext.getCurrentIndex() per determinare se stai visualizzando 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

Ci sono alcuni aspetti importanti da sapere sul modo in cui i callback interagiscono con le operazioni asincrone del datastore. Quando esegui un put() o un delete() utilizzando l'API del datastore asincrono, tutti i callback Pre* che hai registrato verranno eseguiti in modo sincrono. Anche i callback Post* verranno eseguiti in modo sincrono, ma non finché non richiami uno dei metodi Future.get() per recuperare il risultato dell'operazione.

Utilizzo delle richiamate con i servizi App Engine

I callback, come qualsiasi altro codice nell'applicazione, hanno accesso all'intera gamma di servizi di backend di App Engine e puoi definirne quanti vuoi in una singola 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

Quando implementi i callback, devi tenere presenti una serie di errori comuni.

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 in merito all'ordine di esecuzione callback

I callback Pre* vengono sempre eseguiti prima di quelli di Post*, ma non è sicuro fare ipotesi sull'ordine di esecuzione di un callback Pre* rispetto ad altri callback Pre*, né è sicuro fare ipotesi sull'ordine di esecuzione di un callback Post* 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 completare 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) {
        Future result = ds.put(order);
        // ERROR! Never calls result.get() so collectSample() will not run.
    }
}

Evita 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);
    }
}

Utilizzo delle richiamate con Eclipse

Se stai sviluppando la tua app con Eclipse, dovrai eseguire un numero ridotto di passaggi di configurazione per utilizzare i callback del datastore. Questi passaggi si riferiscono a Eclipse 3.7. Prevediamo di rendere superflui questi passaggi in una versione futura del plug-in di Google per Eclipse.

  • Apri la finestra di dialogo Proprietà del progetto (Progetto > Proprietà)
  • Apri la finestra di dialogo per l'elaborazione delle annotazioni (Compilatore Java > Elaborazione delle annotazioni)
  • Seleziona "Abilita elaborazione annotazioni"
  • Seleziona "Abilita elaborazione nell'editor"
  • Apri la finestra di dialogo Percorso di fabbrica (Compilatore Java > Elaborazione delle annotazioni > Percorso di 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"

Puoi verificare che i callback siano configurati correttamente implementando un metodo con più callback (vedi lo snippet di codice in One Callback Per Method). Ciò dovrebbe generare un errore del compilatore.