Transações

O Cloud Datastore oferece suporte a 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 Cloud Datastore 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 Cloud Datastore encontrar um erro interno.

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

Transações são um recurso opcional. Elas não são obrigatórias para realizar operações do Cloud Datastore.

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

Java 8

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

Java 7

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 nossos exemplos mais sucintos, às vezes omitimos o bloco finally que realiza 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 Cloud Datastore para um grupo de entidades, e todas as operações são aplicadas como um grupo, ou não são aplicadas, se a transação falhar.

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 nesse 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 em um grupo são armazenadas no mesmo nó do Cloud 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:

Java 8

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

Java 7

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. No exemplo abaixo, é criada a chave de uma entidade MessageBoard. Essa chave é usada para criar e manter uma entidade Message que reside no mesmo grupo que MessageBoard:

Java 8

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

Java 7

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

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

Transaction txn = datastore.beginTransaction();

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);
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 em um único grupo. A diferença é que, ao iniciar a transação, é preciso especificar que ela seja entre grupos por meio de TransactionOptions:

Java 8

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

Java 7

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 Cloud Datastore impõe restrições sobre o que pode ser feito dentro de uma única transação.

Todas as operações do Cloud Datastore em uma transação precisarão funcionar em entidades no mesmo grupo de entidades se a transação for uma transação de grupo único ou em entidades em um máximo de vinte e cinco grupos de entidades se a transação for uma transação entre grupos. Isso inclui consultar entidades por ancestral, recuperar entidades por chave, atualizar entidades e excluir entidades. Cada entidade raiz pertence a um grupo de entidades separado. 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 Cloud Datastore usa o controle de simultaneidade otimista verificando a hora da atualização mais recente dos grupos de entidades usados na transação. Ao executar o commit de uma transação nos grupos de entidades, o Cloud Datastore verifica novamente o horário da última atualização dos grupos de entidades usados nessa transação. Se esse horário tiver sido alterado desde a verificação inicial, uma exceção será gerada. Para uma explicação de grupos de entidades, consulte a página Visão geral do Cloud Datastore.

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 Cloud Datastore é mais próximo do commit de leitura. Dentro das transações, o isolamento serializável é aplicado. Isso significa que outra transação não pode modificar simultaneamente os dados lidos ou modificados por essa transação. Para mais informações sobre os níveis de isolamento, consulte a wiki sobre isolamento serializável e o artigo Isolamento da transação.

Em uma transação, todas as leituras refletem o estado atual, consistente do Cloud Datastore no momento em que a transação começou. Consultas e recebimentos dentro de uma transação têm a garantia de ver um instantâneo consistente único do Cloud 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, essas consultas retornam o conjunto completo e correto de entidades de resultado, sem os falsos positivos ou negativos descritos em Isolamento da transação 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 Cloud 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, uma consulta ou um recebimento retornará a versão original da entidade desde o início da transação, ou nada se ela não existia.

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 seu valor atual. Como a API Cloud 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.

Java 8

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

Java 7

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, o processo falhará com uma 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:

Java 8

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

Java 7

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 já mencionado, uma transação é necessária para processar o caso em que outro usuário está tentando criar ou atualizar uma entidade com o mesmo código de string. Sem uma transação, se a entidade não existir e dois usuários tentarem criá-la, a segunda substituirá a primeira sem saber disso. 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. Você não precisa criar um loop de repetição em cada transação.

Por fim, você pode usar uma transação para ler um instantâneo consistente do Cloud 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.

Java 8

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

Java 7

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

Você pode enfileirar uma tarefa como parte de uma transação do Cloud Datastore. Dessa maneira, a tarefa só é enfileirada, com garantia de enfileiramento, caso a transação seja confirmada com êxito. 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.

As tarefas transacionais são úteis porque permitem listar ações que não sejam do Cloud Datastore com uma transação do Cloud Datastore, como por exemplo enviar um e-mail para confirmar uma compra. Você também pode vincular ações do Cloud Datastore à transação, como confirmar alterações feitas em grupos de entidades fora da transação, se e somente se a transação for bem-sucedida.

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

Java 8

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

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

// ...

txn.commit();

Java 7

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

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

// ...

txn.commit();
Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…

Ambiente padrão do App Engine para Java 8