API Async Datastore

A API Async Datastore permite-lhe fazer chamadas paralelas e não bloqueadoras para o armazenamento de dados e obter os resultados destas chamadas num ponto posterior no processamento do pedido. Esta documentação descreve os seguintes aspetos da API Async Datastore:

Trabalhar com o serviço Async Datastore

Com a API Async Datastore, faz chamadas ao Datastore através de métodos da interface AsyncDatastoreService. Obtém este objeto chamando o método de classe getAsyncDatastoreService() da classe DatastoreServiceFactory.

import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;

// ...
AsyncDatastoreService datastore = DatastoreServiceFactory.getAsyncDatastoreService();

AsyncDatastoreService suporta as mesmas operações que DatastoreService, exceto que a maioria dos métodos devolve imediatamente um Future cujo resultado pode bloquear num ponto posterior. Por exemplo, DatastoreService.get() devolve uma Entidade, mas AsyncDatastoreService.get() devolve um Future<Entity>.

// ...

Key key = KeyFactory.createKey("Employee", "Max");
// Async call returns immediately
Future<Entity> entityFuture = datastore.get(key);

// Do other stuff while the get operation runs in the background...

// Blocks if the get operation has not finished, otherwise returns instantly
Entity entity = entityFuture.get();

Nota: as exceções não são lançadas até chamar o método get(). Chamar este método permite-lhe verificar se a operação assíncrona foi bem-sucedida.

Se tiver um AsyncDatastoreService, mas precisar de executar uma operação de forma síncrona, invoque o método AsyncDatastoreService adequado e, em seguida, bloqueie imediatamente o resultado:

// ...

Entity entity = new Employee("Employee", "Alfred");
// ... populate entity properties

// Make a sync call via the async interface
Key key = datastore.put(key).get();

Trabalhar com transações assíncronas

As chamadas API de armazenamento de dados assíncronas podem participar em transações tal como as chamadas síncronas. Segue-se uma função que ajusta o salário de um Employee e escreve uma entidade SalaryAdjustment adicional no mesmo grupo de entidades que o Employee, tudo numa única transação.

void giveRaise(AsyncDatastoreService datastore, Key employeeKey, long raiseAmount)
        throws Exception {
    Future<Transaction> txn = datastore.beginTransaction();

    // Async call to lookup the Employee entity
    Future<Entity> employeeEntityFuture = datastore.get(employeeKey);

    // Create and put a SalaryAdjustment entity in parallel with the lookup
    Entity adjustmentEntity = new Entity("SalaryAdjustment", employeeKey);
    adjustmentEntity.setProperty("adjustment", raiseAmount);
    adjustmentEntity.setProperty("adjustmentDate", new Date());
    datastore.put(adjustmentEntity);

    // Fetch the result of our lookup to make the salary adjustment
    Entity employeeEntity = employeeEntityFuture.get();
    long salary = (Long) employeeEntity.getProperty("salary");
    employeeEntity.setProperty("salary", salary + raiseAmount);

    // Re-put the Employee entity with the adjusted salary.
    datastore.put(employeeEntity);
    txn.get().commit(); // could also call txn.get().commitAsync() here
}

Este exemplo ilustra uma diferença importante entre chamadas assíncronas sem transações e chamadas assíncronas com transações. Quando não está a usar uma transação, a única forma de garantir que uma chamada assíncrona individual foi concluída é obter o valor de retorno do Future que foi devolvido quando a chamada foi feita. Quando usa uma transação, a chamada Transaction.commit() bloqueia o resultado de todas as chamadas assíncronas feitas desde o início da transação antes de a confirmar.

Assim, no exemplo acima, embora a nossa chamada assíncrona para inserir a entidade SalaryAdjustment possa ainda estar pendente quando chamamos commit(), a confirmação não ocorre até que a inserção seja concluída. Da mesma forma, se optar por chamar commitAsync() em vez de commit(), invocar get() no Future devolvido por commitAsync() bloqueia até que todas as chamadas assíncronas pendentes sejam concluídas.

Nota: as transações estão associadas a uma discussão específica e não a uma instância específica de DatastoreService ou AsyncDatastoreService. Isto significa que, se iniciar uma transação com um DatastoreService e executar uma chamada assíncrona com um AsyncDatastoreService, a chamada assíncrona participa na transação. Em alternativa, para o dizer de forma mais concisa, DatastoreService.getCurrentTransaction() e AsyncDatastoreService.getCurrentTransaction() devolvem sempre o mesmo Transaction.

Trabalhar com futuros

O Javadoc futuro explica a maioria do que precisa de saber para trabalhar com êxito com um Future devolvido pela API Async Datastore, mas existem algumas coisas específicas do App Engine das quais tem de ter conhecimento:

Consultas assíncronas

Atualmente, não expomos uma API explicitamente assíncrona para consultas. No entanto, quando invoca PreparedQuery.asIterable(), PreparedQuery.asIterator() ou PreparedQuery.asList(FetchOptions fetchOptions), o DatastoreService e o AsyncDatastoreService são devolvidos imediatamente e pré-buscam os resultados de forma assíncrona. Isto permite que a sua aplicação execute tarefas em paralelo enquanto os resultados da consulta são obtidos.

// ...

Query q1 = new Query("Salesperson");
q1.setFilter(new FilterPredicate("dateOfHire", FilterOperator.LESS_THAN, oneMonthAgo));

// Returns instantly, query is executing in the background.
Iterable<Entity> recentHires = datastore.prepare(q1).asIterable();

Query q2 = new Query("Customer");
q2.setFilter(new FilterPredicate("lastContact", FilterOperator.GREATER_THAN, oneYearAgo));

// Also returns instantly, query is executing in the background.
Iterable<Entity> needsFollowup = datastore.prepare(q2).asIterable();

schedulePhoneCall(recentHires, needsFollowUp);

Quando usar chamadas de armazenamento de dados assíncronas

As operações expostas pela interface DatastoreService são síncronas. Por exemplo, quando chama DatastoreService.get(), o seu código é bloqueado até que a chamada para o arquivo de dados seja concluída. Se a única coisa que a sua aplicação precisa de fazer é renderizar o resultado do get() em HTML, bloquear até que a chamada esteja concluída é perfeitamente razoável. No entanto, se a sua aplicação precisar do resultado do get() mais o resultado de um Query para renderizar a resposta e se o get() e o Query não tiverem dependências de dados, então esperar até que o get() seja concluído para iniciar o Query é uma perda de tempo. Segue-se um exemplo de código que pode ser melhorado através da API assíncrona:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Key empKey = KeyFactory.createKey("Employee", "Max");

// Read employee data from the Datastore
Entity employee = datastore.get(empKey); // Blocking for no good reason!

// Fetch payment history
Query query = new Query("PaymentHistory");
PreparedQuery pq = datastore.prepare(query);
List<Entity> result = pq.asList(FetchOptions.Builder.withLimit(10));
renderHtml(employee, result);

Em vez de aguardar a conclusão da get(), use uma instância de AsyncDatastoreService para executar a chamada de forma assíncrona:

AsyncDatastoreService datastore = DatastoreServiceFactory.getAsyncDatastoreService();
Key empKey = KeyFactory.createKey("Employee", "Max");

// Read employee data from the Datastore
Future<Entity> employeeFuture = datastore.get(empKey); // Returns immediately!

// Fetch payment history for the employee
Query query = new Query("PaymentHistory", empKey);
PreparedQuery pq = datastore.prepare(query);

// Run the query while the employee is being fetched
List<Entity> result = pq.asList(FetchOptions.Builder.withLimit(10));
// Implicitly performs query asynchronously
Entity employee = employeeFuture.get(); // Blocking!
renderHtml(employee, result); 

As versões síncronas e assíncronas deste código usam quantidades semelhantes de CPU (afinal, ambas realizam a mesma quantidade de trabalho), mas, uma vez que a versão assíncrona permite que as duas operações da base de dados sejam executadas em paralelo, a versão assíncrona tem uma latência inferior. Em geral, se precisar de realizar várias operações de armazenamento de dados que não tenham dependências de dados, o AsyncDatastoreService pode melhorar significativamente a latência.