Chamadas de retorno do Datastore

Nota: os callbacks não são acionados se a app do App Engine for chamada por outra app através da API Remote.

Uma chamada de retorno permite-lhe executar código em vários pontos do processo de persistência. Pode usar estes callbacks para implementar facilmente lógica multifuncional, como registo, amostragem, decoração, auditoria e validação (entre outras coisas). Atualmente, a API Datastore suporta callbacks que são executados antes e depois das operações put() e delete().

PrePut

Quando regista um callback PrePut com um tipo específico, o callback é invocado antes de qualquer entidade desse tipo ser colocada (ou, se não for fornecido nenhum tipo, antes de qualquer entidade ser colocada):

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 tem de ser um método de instância que devolve void, aceita um PutContext como o seu único argumento, não gera exceções verificadas e pertence a uma classe com um construtor sem argumentos. Quando uma chamada de retorno PrePut gera uma exceção, não são executadas mais chamadas de retorno, e a operação put() gera a exceção antes de serem emitidos RPCs para o repositório de dados.

PostPut

Quando regista uma chamada de retorno PostPut com um tipo específico, a chamada de retorno é invocada depois de qualquer entidade desse tipo ser colocada (ou, se não for fornecido nenhum tipo, depois de qualquer entidade ser 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 tem de ser um método de instância que devolve void, aceita um PutContext como o seu único argumento, não gera exceções verificadas e pertence a uma classe com um construtor sem argumentos. Quando um PostPut callback gera uma exceção, não são executados mais callbacks, mas o resultado da operação put() não é afetado. Tenha em atenção que, se a própria operação put() falhar, as chamadas de retorno PostPut não são invocadas. Além disso, as PostPut chamadas de retorno associadas a operações put() transacionais não são executadas até que a transação seja confirmada com êxito.

PreDelete

Quando regista um callback PreDelete com um tipo específico, o callback é invocado antes de qualquer entidade desse tipo ser eliminada (ou, se não for fornecido nenhum tipo, antes de qualquer entidade ser eliminada):

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 tem de ser um método de instância que devolve void, aceita um DeleteContext como o seu único argumento, não gera exceções verificadas e pertence a uma classe com um construtor sem argumentos. Quando um callback PreDelete gera uma exceção, não são executados mais callbacks e a operação delete() gera a exceção antes de serem emitidos RPCs para o repositório de dados.

PostDelete

Quando regista uma chamada de retorno PostDelete com um tipo específico, a chamada de retorno é invocada depois de qualquer entidade desse tipo ser eliminada (ou, se não for fornecido nenhum tipo, depois de qualquer entidade ser eliminada):

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 tem de ser um método de instância que devolve void, aceita um DeleteContext como o seu único argumento, não gera exceções verificadas e pertence a uma classe com um construtor sem argumentos. Quando um PostDelete callback gera uma exceção, não são executados mais callbacks, mas o resultado da operação delete() não é afetado. Tenha em atenção que, se a própria operação delete() falhar, as chamadas de retorno PostDelete não são invocadas. Além disso, as PostDelete chamadas de retorno associadas a operações delete() transacionais não são executadas até que a transação seja confirmada com êxito.

PreGet

Pode registar um PreGet callback para ser chamado antes de obter entidades de alguns tipos específicos (ou todos os tipos). Pode usar esta opção, por exemplo, para intercetar alguns pedidos GET e obter os dados de uma cache em vez de os obter 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

Pode registar uma PreQuery devolução de chamada a ser chamada antes de executar consultas para tipos específicos (ou todos os tipos). Pode usar esta opção, por exemplo, para adicionar um filtro de igualdade com base no utilizador com sessão iniciada. A função de retorno de chamada recebe a consulta. Se a modificar, a função de retorno de chamada pode fazer com que seja executada uma consulta diferente. Em alternativa, a função de retorno de chamada pode gerar uma exceção não verificada para impedir a execução da consulta.

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

Pode registar uma PostLoad devolução de chamada para ser chamada após o carregamento de entidades de tipos específicos (ou todos os tipos). Aqui, "carregar" pode significar o resultado de um pedido ou uma consulta. Isto é ú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 executa uma operação em lote (por exemplo, um put() com várias entidades), as suas chamadas de retorno são invocadas uma vez por entidade. Pode aceder ao lote completo de objetos chamando CallbackContext.getElements() no argumento do seu método de callback. Isto permite-lhe implementar callbacks que operam em todo o lote, em vez de um elemento de cada 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 precisar que o seu callback seja executado apenas uma vez por lote, use CallbackContext.getCurrentIndex() para determinar se está a analisar 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

Existem alguns aspetos importantes a ter em conta sobre a forma como os callbacks interagem com as operações de armazenamento de dados assíncronas. Quando executa um put() ou um delete() através da API datastore assíncrona, todas as chamadas de retorno Pre* que registou são executadas de forma síncrona. As suas chamadas de retorno Post* também são executadas de forma síncrona, mas apenas quando chamar qualquer um dos métodos Future.get() para obter o resultado da operação.

Usar callbacks com os serviços do App Engine

Os callbacks, tal como qualquer outro código na sua aplicação, têm acesso à gama completa de serviços de back-end do App Engine, e pode definir quantos quiser numa ú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

Existem vários erros comuns a ter em atenção ao implementar callbacks.

Não manter o 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 faça suposições sobre a ordem de execução da chamada de retorno

As chamadas de retorno Pre* são sempre executadas antes das chamadas de retorno Post*, mas não é seguro fazer suposições sobre a ordem em que uma chamada de retorno Pre* é executada relativamente a outras chamadas de retorno Pre*, nem é seguro fazer suposições sobre a ordem em que uma chamada de retorno Post* é executada relativamente a outras chamadas de retorno 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!");
        }
    }
}

Uma chamada de retorno por método

Embora uma classe possa ter um número ilimitado de métodos de retorno de chamada, um único método só pode ser associado a um único retorno de chamada.

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 esqueça de obter resultados assíncronos

As funções de retorno de chamada Post* não são executadas até chamar Future.get() para obter o resultado da operação. Se se esquecer de chamar Future.get() antes de terminar a manutenção do pedido HTTP, os seus callbacks Post* não sã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.
    }
}

Evite repetições infinitas

Se realizar operações de armazenamento de dados nas suas chamadas de retorno, tenha cuidado para não entrar num 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);
    }
}

Usar callbacks com o Eclipse

Se estiver a desenvolver a sua app com o Eclipse, tem de realizar um pequeno número de passos de configuração para usar os callbacks da base de dados. Estes passos destinam-se ao Eclipse 3.7. Esperamos tornar estes passos desnecessários numa versão futura do plug-in Google para Eclipse.

  • Abra a caixa de diálogo Propriedades do seu projeto (Projeto > Propriedades)
  • Abra a caixa de diálogo Processamento de anotações (Java Compiler > Annotation Processing)
  • Selecione "Ativar processamento de anotações"
  • Selecione "Ativar processamento no editor"
  • Abra a caixa de diálogo Factory Path (Java Compiler > Annotation Processing > Factory Path)
  • Clique em "Adicionar JARs externos"
  • Selecione <SDK_ROOT>/lib/impl/appengine-api.jar (onde SDK_ROOT é o diretório de nível superior da instalação do SDK)
  • Clique em "OK"

Pode verificar se os callbacks estão configurados corretamente implementando um método com vários callbacks (consulte o fragmento do código em Um callback por método). Isto deve gerar um erro do compilador.