Callback di Datastore

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). Al momento l'API Datastore 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 verrà invocato prima che venga inserita qualsiasi entità di quel tipo (o, se non viene fornito alcun tipo, prima che venga inserita 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 un PutContext come unico argomento, non genera eccezioni controllate e appartiene a una classe con un costruttore senza argomenti. 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 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 un PutContext come unico argomento, non genera eccezioni controllate e appartiene a una classe con un costruttore senza argomenti. 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() stessa non va a buon fine, i callback PostPut non verranno richiamati. Inoltre, i PostPut callback associati alle operazioni put() di transazione 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 un DeleteContext come unico argomento, non genera eccezioni controllate e appartiene a una classe con un costruttore senza argomenti. 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 un DeleteContext come unico argomento, non genera eccezioni controllate e appartiene a una classe con un costruttore senza argomenti. 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() stessa non va a buon fine, i callback PostDelete non verranno richiamati. Inoltre, i PostDelete callback associati alle operazioni delete() di transazione 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 callback PreQuery da chiamare prima di eseguire query per tipi specifici (o per tutti i tipi). Ad esempio, puoi utilizzarlo per aggiungere un filtro di uguaglianza in base all'utente che ha eseguito l'accesso. Al callback viene passata la query; modificandola, il callback può causare l'esecuzione di un'altra query. 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" potrebbe indicare il risultato di un get o di una query. Questa funzionalità è 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 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 metodo di callback. In questo modo puoi implementare i callback che operano sull'intero batch anziché 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 async datastore, tutti i Pre* callback che hai registrato verranno eseguiti in modo sincrono. Anche i tuoi callback Post* verranno eseguiti in modo sincrono, ma non finché non chiami uno dei metodi Future.get() per recuperare il risultato dell'operazione.

Utilizzare i rilanci con i servizi App Engine

I richiami, come qualsiasi altro codice dell'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

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 supposizioni sull'ordine in cui viene eseguito un callback Pre* rispetto ad altri callback Pre*, né sull'ordine in cui viene eseguito 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!");
        }
    }
}

Una chiamata 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 il servizio 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.
    }
}

Evitare i loop infiniti

Se esegui operazioni sul datastore nei callback, fai attenzione a non cadere in un ciclo 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, dovrai eseguire alcuni passaggi di configurazione per utilizzare i callback del datastore. Questi passaggi si riferiscono a Eclipse 3.7. Prevediamo di rendere questi passaggi non necessari in una release futura del plug-in Google per Eclipse.

  • Apri la finestra di dialogo Proprietà del progetto (Progetto > Proprietà).
  • Apri la finestra di dialogo Elaborazione annotazioni (Compilatore Java > Elaborazione annotazioni)
  • Seleziona "Attiva l'elaborazione delle annotazioni"
  • Seleziona "Attiva l'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".

Puoi verificare che i callback siano configurati correttamente implementando un metodo con più callback (vedi lo snippet di codice in Un callback per metodo). Dovrebbe generare un errore del compilatore.