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). L'API Datastore supporta attualmente 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 che venga inserita qualsiasi entità di quel tipo (o, se non viene fornito alcun tipo, prima dell'inserimento di 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 vengano inviate RPC 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 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 riesce, i callback PostPut non verranno richiamati. Inoltre, i callback PostPut associati alle operazioni put() transazionali 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 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 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 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 riesce, i callback PostDelete non verranno richiamati. Inoltre, i callback PostDelete associati alle operazioni delete() transazionali 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 PreQuery da chiamare prima di eseguire query tipi specifici (o di tutti i tipi). Ad esempio, puoi utilizzarlo per aggiungere un filtro di uguaglianza in base all'utente che ha eseguito l'accesso. Il callback viene passato la query; modificandolo, il callback può causare l'esecuzione di una query diversa. 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 in corso" potrebbe indicare il risultato di un comando get o di una query. Ciò è 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 in 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 tuo metodo di callback. In questo modo puoi implementare i 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 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 di archiviazione dati asincrona, tutti i Pre* callback che hai registrato verranno eseguiti in modo sincrono. I callback Post* verranno eseguiti anche in modalità sincrona, ma non finché non richiamerai uno dei metodi Future.get() per recuperare il risultato dell'operazione.

Utilizzare i rilanci con i servizi App Engine

I callback, come qualsiasi altro codice nella tua 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 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 sull'ordine di esecuzione dei callback

I callback Pre* verranno sempre eseguiti prima dei callback Post*, ma non è sicuro fare alcuna ipotesi sull'ordine in cui viene eseguito un callback Pre* rispetto ad altri callback Pre*, né fare ipotesi sull'ordine in cui un callback Post* viene eseguito 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 terminare 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 i 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);
    }
}

Utilizzare i Callback con Eclipse

Se stai sviluppando la tua app con Eclipse, devi eseguire un numero ridotto di passaggi di configurazione per utilizzare i callback del datastore. Questi passaggi si riferiscono a Eclipse 3.7. Questi passaggi non saranno più necessari in una versione futura del Google Plugin For Eclipse.

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

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