Callbacks do Datastore

Observação: callbacks não serão acionados se o aplicativo do App Engine for chamado por outro aplicativo que use a Remote API.

Com um callback, é possível executar o código em vários pontos no processo de persistência. Você pode usar esses callbacks para implementar facilmente uma lógica multifuncional, como registro, amostragem, decoração, auditoria, validação etc. No momento, a API Datastore é compatível com callbacks executados antes e depois das operações put() e delete().

PrePut

Quando você registra um callback PrePut com um tipo específico, o callback é invocado antes de qualquer entidade desse tipo ser inserida (ou, se nenhum tipo for fornecido, antes de qualquer entidade ser inserida):

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

Um método anotado com PrePut precisa ser um método de instância que retorna void, aceita PutContext como seu único argumento, não gera nenhuma exceção verificada e pertence a uma classe com um construtor no-arg. Quando um callback PrePut gera uma exceção, nenhum callback novo é executado, e a operação put() gera a exceção antes que as RPCs sejam emitidas para o armazenamento de dados.

PostPut

Quando você registra um callback PostPut com um tipo específico, ele é invocado depois que qualquer entidade desse tipo é colocada (ou, se nenhum tipo for fornecido, depois que qualquer entidade for colocada).

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

Um método anotado com PostPut precisa ser um método de instância que retorna void, aceita PutContext como seu único argumento, não gera nenhuma exceção verificada e pertence a uma classe com um construtor no-arg. Quando um callback PostPut gera uma exceção, nenhum callback novo é executado, mas o resultado da operação put() não é afetado. Observe que, se a operação put() propriamente dita falhar, os callbacks PostPut não serão invocados. Além disso, callbacks PostPut associados a operações transacionais put() não serão executados até que a transação seja concluída.

PreDelete

Ao registrar um callback PreDelete com um tipo específico, o callback será invocado antes que qualquer entidade desse tipo seja excluída (ou, se nenhum tipo for fornecido, antes de qualquer entidade ser excluída):

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

Um método anotado com PreDelete precisa ser um método de instância que retorna void, aceita DeleteContext como seu único argumento, não gera nenhuma exceção verificada e pertence a uma classe com um construtor no-arg. Quando um callback PreDelete gera uma exceção, nenhum callback novo é executado, e a operação delete() gera a exceção antes que as RPCs sejam emitidas para o armazenamento de dados.

PostDelete

Quando você registra um callback PostDelete com um tipo específico, ele é invocado depois que qualquer entidade desse tipo é excluída (ou, se nenhum tipo for fornecido, depois que qualquer entidade for excluída):

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

Um método anotado com PostDelete precisa ser um método de instância que retorna void, aceita DeleteContext como seu único argumento, não gera nenhuma exceção verificada e pertence a uma classe com um construtor no-arg. Quando um callback PostDelete gera uma exceção, nenhum callback novo é executado, mas o resultado da operação delete() não é afetado. Observe que, se a operação delete() propriamente dita falhar, os callbacks PostDelete não serão invocados. Além disso, callbacks PostDelete associados a operações transacionais delete() não serão executados até que a transação seja concluída.

PreGet

É possível registrar um callback PreGet para ser chamado antes de conseguir entidades de alguns tipos específicos (ou de todos os tipos). Você pode usar isso, por exemplo, para interceptar algumas solicitações "get" e buscar os dados de um cache em vez do armazenamento de dados.

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

É possível registrar um callback PreQuery para ser chamado antes de executar consultas para tipos específicos (ou todos os tipos). Você pode usar isso, por exemplo, para adicionar um filtro de igualdade com base no usuário conectado. O callback é encaminhado para a consulta. Ao modificá-lo, uma consulta diferente pode ser executada no lugar dele. Além disso, o callback pode gerar uma exceção não verificada para impedir que a consulta seja executada.

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

É possível registrar um callback PostLoad para ser chamado depois de carregar entidades de tipos específicos (ou todos os tipos). Neste contexto, "carregar" pode significar o resultado de uma busca ou de uma consulta. Isso é útil para decorar entidades carregadas.

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

Operações em lote

Quando você executa uma operação em lote (um put() com várias entidades, por exemplo), os callbacks são invocados uma vez por entidade. É possível acessar todo o lote de objetos chamando CallbackContext.getElements() no argumento para seu método de callback. Isso permite que você implemente callbacks que operam em todo o lote em vez de um elemento por 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.");
        }
    }
}

Se você precisar que seu callback seja executado apenas uma vez por lote, use CallbackContext.getCurrentIndex() para determinar se você está analisando o primeiro elemento do 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());
        }
    }
}

Operações assíncronas

Há alguns fatos importantes sobre como os callbacks interagem com as operações de armazenamento de dados assíncronas. Quando você executa put() ou delete() usando a API Async Datastore, todos os callbacks Pre* registrados serão executados de maneira síncrona. Seus callbacks Post* também serão executados de maneira síncrona, mas não até chamar um método Future.get() para recuperar o resultado da operação.

Como usar callbacks com serviços do App Engine

Os callbacks, como qualquer outro código no seu aplicativo, têm acesso a toda a gama de serviços de back-end do App Engine, e você pode definir tantos quanto quiser em uma única 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();
        // ...
    }
}

Erros comuns a evitar

Há uma série de erros comuns que você precisa conhecer ao implementar retornos de chamada.

Não manter estado não 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());
        }
    }
}

Não pressupor a ordem de execução de callback

Os callbacks Pre* sempre serão executados antes dos callbacks Post*. Entretanto, não é seguro supor sobre a ordem em que um callback Pre* é executado em relação a outros callbacks Pre*, nem supor sobre a ordem em que um callback Post* é executado em relação a outros callbacks 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!");
        }
    }
}

Um callback por método

Mesmo que uma classe possa ter um número ilimitado de métodos de callback, um único método só pode ser associado a um único 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) { }
}

Não se esquecer de recuperar resultados assíncronos

Callbacks Post* não são executados até que você chame Future.get() para recuperar o resultado da operação. Se você se esquecer de ligar Future.get() antes de concluir a solicitação HTTP, os callbacks Post* não serão executados.

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 loops infinitos

Se você executar operações de armazenamento de dados nos callbacks, evite loops infinitos.

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

Como usar callbacks com o Eclipse

Se você estiver desenvolvendo seu aplicativo com o Eclipse, você precisará executar um pequeno número de etapas de configuração para usar os callbacks do armazenamento de dados. Essas etapas são para o Eclipse 3.7. Esperamos tornar essas etapas desnecessárias em uma versão futura do plug-in do Google para o Eclipse.

  • Abra a caixa de diálogo "Properties" para seu Projeto (Project > Properties).
  • Abra a caixa de diálogo "Annotation Processing dialog" (Java Compiler > Annotation Processing).
  • Marque "Enable annotation processing".
  • Marque "Enable processing in editor".
  • Abra a caixa de diálogo "Factory Path" (Java Compiler > Annotation Processing > Factory Path).
  • Clique em "Add External JARs".
  • Selecione <SDK_ROOT>/lib/impl/appengine-api.jar (em que SDK_ROOT é o diretório de nível superior da sua instalação do SDK).
  • Clique em "OK".

Você pode verificar se os retornos de chamada estão configurados corretamente implementando um método com vários retornos de chamada. Consulte o snippet de código em Um retorno de chamada por método. Isso pode gerar um erro no compilador.