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
- Não pressupor a ordem de execução de callback
- Um callback por método
- Não se esquecer de recuperar resultados assíncronos
- Evitar loops infinitos
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) { Futureresult = 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.