Transações

O Datastore é compatível com transações. Transação é uma operação ou um conjunto de operações atômico. Todas as operações na transação ocorrem ou nenhuma delas ocorre. Um aplicativo pode realizar várias operações e cálculos em uma única transação.

Como usar transações

Transação é um conjunto de operações do Datastore que ocorrem em uma ou mais entidades. Cada transação é certamente atômica, o que significa que transações jamais são aplicadas parcialmente. Todas as operações na transação são aplicadas ou nenhuma delas é aplicada. As transações têm uma duração máxima de 60 segundos, com um tempo de expiração por inatividade de 10 segundos após 30 segundos.

Uma operação poderá falhar quando:

  • muitas modificações simultâneas forem tentadas no mesmo grupo de entidades;
  • a transação exceder um limite de recursos;
  • o Datastore encontrar um erro interno.

Em todos esses casos, a API Cloud Datastore gera uma exceção.

As transações são um recurso opcional do Datastore. Elas não precisam ser usadas para realizar operações do Datastore.

Este é um exemplo de atualização do campo chamado vacationDays em uma entidade do tipo Employee chamada Joe:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Transaction txn = datastore.beginTransaction();
try {
  Key employeeKey = KeyFactory.createKey("Employee", "Joe");
  Entity employee = datastore.get(employeeKey);
  employee.setProperty("vacationDays", 10);

  datastore.put(txn, employee);

  txn.commit();
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

Observe que, para manter os exemplos mais sucintos, às vezes omitimos o bloco finally que realiza uma reversão se a transação ainda estiver ativa. Em código de produção, é importante garantir que cada transação seja executada explicitamente ou revertida.

Grupos de entidades

Cada entidade pertence a um grupo, um conjunto de uma ou mais entidades que podem ser manipuladas em uma única transação. Os relacionamentos de grupo de entidades informam ao App Engine que ele precisa armazenar diversas entidades na mesma parte da rede distribuída. Uma transação configura operações do Datastore em um grupo de entidades. Se a transação falhar, todas as operações são aplicadas como um grupo ou não são aplicadas.

Ao criar uma entidade, o aplicativo pode atribuir outra entidade como pai da nova entidade. Com essa atribuição para a nova entidade, ela fará parte do mesmo grupo que a entidade pai.

Uma entidade sem pai é uma entidade raiz. Uma entidade pai também pode ser filho. O caminho para a entidade é uma cadeia de entidades pai que vai dela até a raiz, e os membros desse caminho são os ancestrais dela. O pai de uma entidade é definido quando ela é criada e não pode ser alterado posteriormente.

Todas as entidades com uma raiz determinada como ancestral estão no mesmo grupo. Todas as entidades de um grupo são armazenadas no mesmo nó do Datastore. Uma única transação pode modificar diversas entidades de um único grupo ou adicionar novas entidades ao grupo. Assim, o pai da nova entidade também fará parte desse grupo. O snippet de código a seguir demonstra transações em vários tipos de entidades:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Entity person = new Entity("Person", "tom");
datastore.put(person);

// Transactions on root entities
Transaction txn = datastore.beginTransaction();

Entity tom = datastore.get(person.getKey());
tom.setProperty("age", 40);
datastore.put(txn, tom);
txn.commit();

// Transactions on child entities
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photo = new Entity("Photo", tom.getKey());

// Create a Photo that is a child of the Person entity named "tom"
photo.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photo);
txn.commit();

// Transactions on entities in different entity groups
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photoNotAChild = new Entity("Photo");
photoNotAChild.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photoNotAChild);

// Throws IllegalArgumentException because the Person entity
// and the Photo entity belong to different entity groups.
txn.commit();

Como criar uma entidade em um grupo específico

Quando seu aplicativo cria uma nova entidade, você pode atribuí-la a um grupo de entidades, fornecendo a chave de outra entidade. O exemplo abaixo cria a chave de uma entidade MessageBoard e usa essa chave para criar e manter uma entidade Message que reside no mesmo grupo de entidades que o MessageBoard:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

String messageTitle = "Some Title";
String messageText = "Some message.";
Date postDate = new Date();

Key messageBoardKey = KeyFactory.createKey("MessageBoard", boardName);

Entity message = new Entity("Message", messageBoardKey);
message.setProperty("message_title", messageTitle);
message.setProperty("message_text", messageText);
message.setProperty("post_date", postDate);

Transaction txn = datastore.beginTransaction();
datastore.put(txn, message);

txn.commit();

Como usar transações entre grupos

As transações entre grupos, também chamadas de transações XG, operam em vários grupos de entidades. Elas têm o mesmo comportamento das transações de grupo único descritas acima, exceto que as entre grupos não falham se o código tentar atualizar entidades de mais de um grupo.

O uso de uma transação entre grupos é semelhante ao uso da transação de um único grupo. A diferença é que, ao iniciar a transação, é preciso especificar que ela seja entre grupos por meio de TransactionOptions:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
TransactionOptions options = TransactionOptions.Builder.withXG(true);
Transaction txn = datastore.beginTransaction(options);

Entity a = new Entity("A");
a.setProperty("a", 22);
datastore.put(txn, a);

Entity b = new Entity("B");
b.setProperty("b", 11);
datastore.put(txn, b);

txn.commit();

O que pode ser feito em uma transação

O Datastore impõe restrições sobre o que pode ser feito dentro de uma única transação.

Quando a transação é feita em um único grupo, todas as operações do Datastore precisam operar em entidades do mesmo grupo. Quando a transação é feita entre grupos, ela precisa operar em um máximo de 25 grupos. Isso inclui consultar entidades por ancestral, recuperar entidades por chave, atualizar entidades e excluir entidades. Cada entidade raiz pertence a um grupo separado de entidades. Dessa forma, uma única transação não pode criar nem operar em mais de uma entidade raiz, a menos que seja uma transação entre grupos.

Quando duas ou mais transações tentam modificar entidades simultaneamente em um ou mais grupos de entidades comuns, somente a primeira transação que faz o commit das suas alterações pode ser bem-sucedida. Todas as outras falharão no commit. Por causa desse design, usar grupos de entidades limita o número de gravações simultâneas que você pode fazer em qualquer entidade nos grupos. Quando uma transação é iniciada, o Datastore usa o controle de simultaneidade otimista verificando o horário da última atualização dos grupos de entidades usados na transação. Após a confirmação de uma transação nos grupos de entidades, o Datastore verifica novamente o horário da última atualização dos grupos de entidades usados na transação. Se esse horário tiver sido alterado desde a verificação inicial, uma exceção será gerada.

Um aplicativo pode realizar uma consulta durante uma transação, mas somente se incluir um filtro de ancestrais. Um aplicativo também pode receber entidades do Datastore por chave durante uma transação. É possível preparar as chaves antes da transação. Como alternativa, você pode criá-las na transação com nomes ou códigos de chave.

Isolamento e consistência

Fora das transações, o nível de isolamento do Datastore está mais próximo da confirmação de leitura. Dentro, por sua vez, o isolamento serializável é imposto. Isso significa que outra transação não pode modificar simultaneamente os dados lidos ou modificados por essa transação.

Em uma transação, todas as leituras refletem o estado atual e consistente do Datastore no momento em que a transação foi iniciada. As consultas e os recebimentos dentro de uma transação garantem a visualização de um snapshot único e consistente do Datastore desde o início da transação. Entidades e linhas de índice no grupo de entidades da transação são totalmente atualizadas. Dessa forma, as consultas retornam o conjunto completo e correto de entidades de resultado, sem os falsos positivos ou falsos negativos que podem ocorrer em consultas fora das transações.

A visualização desse instantâneo consistente também se estende a leituras após gravações dentro de transações. Diferentemente da maioria dos bancos de dados, as consultas e os recebimentos dentro de uma transação do Datastore não exibem os resultados de gravações anteriores dentro dessa transação. Mais especificamente, se uma entidade tiver sido modificada ou excluída dentro de uma transação, a consulta ou o recebimento retornará a versão original da entidade desde o início da transação, ou nada, caso ela não exista.

Usos das transações

Neste exemplo, demonstramos um uso das transações: atualização de uma entidade com um novo valor de propriedade relativo ao respectivo valor atual. Como a API Datastore não faz novas tentativas de transação, podemos adicionar lógica para que isso ocorra, caso outra solicitação atualize o mesmo MessageBoard ou qualquer uma das Messages dele ao mesmo tempo.

int retries = 3;
while (true) {
  Transaction txn = datastore.beginTransaction();
  try {
    Key boardKey = KeyFactory.createKey("MessageBoard", boardName);
    Entity messageBoard = datastore.get(boardKey);

    long count = (Long) messageBoard.getProperty("count");
    ++count;
    messageBoard.setProperty("count", count);
    datastore.put(txn, messageBoard);

    txn.commit();
    break;
  } catch (ConcurrentModificationException e) {
    if (retries == 0) {
      throw e;
    }
    // Allow retry to occur
    --retries;
  } finally {
    if (txn.isActive()) {
      txn.rollback();
    }
  }
}

Isso requer uma transação porque o valor poderá ser atualizado por outro usuário depois que esse código buscar o objeto, mas antes de ele salvar o objeto modificado. Sem uma transação, a solicitação do usuário usa o valor de count antes da atualização do outro usuário, e a gravação substitui o novo valor. Com uma transação, o aplicativo é informado sobre a atualização. Se a entidade for atualizada durante a transação, ela falhará com um ConcurrentModificationException. O aplicativo pode repetir a transação para usar os novos dados.

Outro uso comum para transações é buscar uma entidade com uma chave nomeada ou criá-la caso ela ainda não exista:

Transaction txn = datastore.beginTransaction();
Entity messageBoard;
Key boardKey;
try {
  boardKey = KeyFactory.createKey("MessageBoard", boardName);
  messageBoard = datastore.get(boardKey);
} catch (EntityNotFoundException e) {
  messageBoard = new Entity("MessageBoard", boardName);
  messageBoard.setProperty("count", 0L);
  boardKey = datastore.put(txn, messageBoard);
}
txn.commit();

Como antes, uma transação é necessária para manipular o caso quando outro usuário estiver tentando criar ou atualizar uma entidade com o mesmo ID de string. Sem uma transação, se a entidade não existir e dois usuários tentarem criá-la, o segundo substituirá o primeiro sem saber o que aconteceu. Com uma transação, a segunda tentativa falha atomicamente. Se fizer sentido, o aplicativo poderá tentar buscar novamente a entidade e atualizá-la.

Quando uma transação falha, podem ser feitas novas tentativas até que ela seja bem-sucedida. Ou, como alternativa, os usuários podem lidar com o erro por meio da propagação dele até o nível de interface do usuário do app. Não é necessário criar um loop de repetição em cada transação.

Por fim, use a transação para ler um snapshot consistente do Datastore. Isso pode ser útil quando várias leituras são necessárias para renderizar uma página ou exportar dados que precisam ser consistentes. Esses tipos de transações costumam ser chamadas de transações somente leitura, porque elas não realizam gravações. As transações de grupo único somente leitura nunca falham por causa de modificações simultâneas. Dessa forma, você não precisa implementar repetições após uma falha. Porém, as transações entre grupos podem falhar por causa de modificações simultâneas. Dessa maneira, elas devem ter novas tentativas. O commit e a reversão de uma transação somente leitura são de ambiente autônomo.

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

// Display information about a message board and its first 10 messages.
Key boardKey = KeyFactory.createKey("MessageBoard", boardName);

Transaction txn = datastore.beginTransaction();

Entity messageBoard = datastore.get(boardKey);
long count = (Long) messageBoard.getProperty("count");

Query q = new Query("Message", boardKey);

// This is an ancestor query.
PreparedQuery pq = datastore.prepare(txn, q);
List<Entity> messages = pq.asList(FetchOptions.Builder.withLimit(10));

txn.commit();

Enfileiramento de tarefa transacional

É possível enfileirar uma tarefa como parte de uma transação do Datastore, de modo que ela seja enfileirada e garantida para ser enfileirada apenas se a transação for confirmada. Se a transação não for confirmada, a tarefa será enfileirada com garantia. Uma vez enfileirada, não há garantia de que a tarefa seja executada imediatamente e que qualquer operação realizada dentro dela seja executada de maneira independente da transação original. A tarefa é repetida até ter êxito. Isso se aplica a qualquer tarefa enfileirada no contexto de uma transação.

Tarefas transacionais são úteis, porque permitem listar ações que não são do Datastore em uma transação do Datastore (como enviar um e-mail para confirmar uma compra). Também é possível vincular ações do Datastore à transação, por exemplo, executando alterações em grupos de entidades adicionais fora da transação se, e somente se, a transação tiver êxito.

Um aplicativo não pode inserir mais de cinco tarefas transacionais a filas de tarefas durante uma única transação. Tarefas transacionais não podem ter nomes especificados pelo usuário.

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Queue queue = QueueFactory.getDefaultQueue();
Transaction txn = datastore.beginTransaction();
// ...

queue.add(txn, TaskOptions.Builder.withUrl("/path/to/handler"));

// ...

txn.commit();