Transações

O Datastore suporta transações. Uma transação é uma operação ou um conjunto de operações que é atómico: todas as operações na transação ocorrem ou nenhuma delas ocorre. Uma aplicação pode realizar várias operações e cálculos numa única transação.

Usar transações

Uma transação é um conjunto de operações do Datastore numa ou mais entidades. Cada transação tem a garantia de ser atómica, o que significa que as transações nunca 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 de inatividade de 10 segundos após 30 segundos.

Uma operação pode falhar quando:

  • Foram tentadas demasiadas modificações simultâneas no mesmo grupo de entidades.
  • A transação excede um limite de recursos.
  • O Datastore encontra um erro interno.

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

As transações são uma funcionalidade opcional do Datastore. Não tem de usar transações para realizar operações do Datastore.

Segue-se um exemplo de atualização do campo denominado vacationDays numa entidade do tipo Employee denominada 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();
  }
}

Tenha em atenção que, para manter os nossos exemplos mais concisos, por vezes, omitimos o bloco finally que faz uma reversão se a transação ainda estiver ativa. No código de produção, é importante garantir que cada transação é explicitamente confirmada ou revertida.

Grupos de entidades

Cada entidade pertence a um grupo de entidades, um conjunto de uma ou mais entidades que podem ser manipuladas numa única transação. As relações de grupos de entidades indicam ao App Engine que armazene várias entidades na mesma parte da rede distribuída. Uma transação configura operações do Datastore para um grupo de entidades, e todas as operações são aplicadas como um grupo ou não são aplicadas de todo se a transação falhar.

Quando a aplicação cria uma entidade, pode atribuir outra entidade como o elemento principal da nova entidade. A atribuição de um elemento principal a uma nova entidade coloca a nova entidade no mesmo grupo de entidades que a entidade principal.

Uma entidade sem um principal é uma entidade raiz. Uma entidade que é principal de outra entidade também pode ter uma entidade principal. Uma cadeia de entidades superiores de uma entidade até à raiz é o caminho da entidade, e os membros do caminho são os predecessores da entidade. O elemento principal de uma entidade é definido quando a entidade é criada e não pode ser alterado posteriormente.

Todas as entidades com uma determinada entidade raiz como antepassado estão no mesmo grupo de entidades. Todas as entidades num grupo são armazenadas no mesmo nó do Datastore. Uma única transação pode modificar várias entidades num único grupo ou adicionar novas entidades ao grupo tornando o elemento principal da nova entidade um elemento existente no grupo. O seguinte fragmento do código 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();

Criar uma entidade num grupo de entidades específico

Quando a sua aplicação cria uma nova entidade, 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, em seguida, usa essa chave para criar e persistir uma entidade Message que reside no mesmo grupo de entidades que a 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();

Usar transações entre grupos

As transações entre grupos (também denominadas transações XG) operam em vários grupos de entidades, comportando-se como as transações de grupo único descritas acima, exceto que as transações entre grupos não falham se o código tentar atualizar entidades de mais do que um grupo de entidades.

A utilização de uma transação entre grupos é semelhante à utilização de uma transação de grupo único, exceto que tem de especificar que quer que a transação seja entre grupos quando a inicia, através 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 numa transação

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

Todas as operações do Datastore numa transação têm de operar em entidades no mesmo grupo de entidades se a transação for uma transação de grupo único, ou em entidades num máximo de vinte e cinco grupos de entidades se a transação for uma transação entre grupos. Isto inclui a consulta de entidades por ascendente, a obtenção de entidades por chave, a atualização de entidades e a eliminação de entidades. Tenha em atenção que cada entidade raiz pertence a um grupo de entidades separado. Por isso, uma única transação não pode criar nem operar em mais do que uma entidade raiz, a menos que seja uma transação entre grupos.

Quando duas ou mais transações tentam modificar simultaneamente entidades num ou mais grupos de entidades comuns, apenas a primeira transação a confirmar as respetivas alterações é bem-sucedida. Todas as outras falham na confirmação. Devido a este design, a utilização de grupos de entidades limita o número de escritas simultâneas que pode fazer em qualquer entidade nos grupos. Quando uma transação é iniciada, o Datastore usa o controlo de concorrência otimista verificando a hora da última atualização dos grupos de entidades usados na transação. Ao confirmar uma transação para os grupos de entidades, o Datastore volta a verificar a hora da última atualização dos grupos de entidades usados na transação. Se tiver mudado desde a verificação inicial, é gerada uma exceção.

Uma app pode executar uma consulta durante uma transação, mas apenas se incluir um filtro de antepassados. Uma app também pode obter entidades do Datastore por chave durante uma transação. Pode preparar as chaves antes da transação ou criar chaves na transação com nomes ou IDs de chaves.

Isolamento e consistência

Fora das transações, o nível de isolamento do Datastore é o mais próximo do read committed. Dentro das transações, o isolamento serializável é aplicado. Isto significa que outra transação não pode modificar em simultâneo os dados que são lidos ou modificados por esta transação.

Numa 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 as obtenções numa transação têm a garantia de ver um único resumo consistente do Datastore a partir do início da transação. As entidades e as linhas de índice no grupo de entidades da transação são totalmente atualizadas para que as consultas devolvam o conjunto completo e correto de entidades de resultados, sem os falsos positivos ou os falsos negativos que podem ocorrer em consultas fora das transações.

Esta vista de instantâneo consistente também se aplica a leituras após escritas em transações. Ao contrário da maioria das bases de dados, as consultas e as obtenções numa transação do Datastore não veem os resultados das escritas anteriores nessa transação. Especificamente, se uma entidade for modificada ou eliminada numa transação, uma consulta ou um pedido de obtenção devolve a versão original da entidade no início da transação ou nada se a entidade não existisse nessa altura.

Usos para transações

Este exemplo demonstra uma utilização das transações: atualizar uma entidade com um novo valor de propriedade relativo ao respetivo valor atual. Uma vez que a API Datastore não tenta novamente as transações, podemos adicionar lógica para que a transação seja tentada novamente caso outro pedido atualize o mesmo MessageBoard ou qualquer um dos respetivos Messages 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();
    }
  }
}

Isto requer uma transação porque o valor pode ser atualizado por outro utilizador após este código obter o objeto, mas antes de guardar o objeto modificado. Sem uma transação, o pedido do utilizador usa o valor de count antes da atualização do outro utilizador, e a gravação substitui o novo valor. Com uma transação, a aplicação é informada sobre a atualização do outro utilizador. Se a entidade for atualizada durante a transação, a transação falha com um ConcurrentModificationException. A aplicação pode repetir a transação para usar os novos dados.

Outra utilização comum das transações é obter uma entidade com uma chave com nome ou criá-la se ainda não existir:

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

Tal como antes, é necessária uma transação para processar o caso em que outro utilizador está a tentar criar ou atualizar uma entidade com o mesmo ID de string. Sem uma transação, se a entidade não existir e dois utilizadores tentarem criá-la, o segundo substitui o primeiro sem saber que isso aconteceu. Com uma transação, a segunda tentativa falha de forma atómica. Se fizer sentido, a aplicação pode tentar novamente obter a entidade e atualizá-la.

Quando uma transação falha, pode fazer com que a sua app tente novamente a transação até ter êxito ou pode permitir que os utilizadores lidem com o erro propagando-o ao nível da interface do utilizador da sua app. Não tem de criar um ciclo de repetição em torno de cada transação.

Por último, pode usar uma transação para ler uma imagem consistente do Datastore. Isto pode ser útil quando são necessárias várias leituras para renderizar uma página ou exportar dados que têm de ser consistentes. Estes tipos de transações são frequentemente denominados transações apenas de leitura, uma vez que não realizam escritas. As transações de grupo único só de leitura nunca falham devido a modificações simultâneas, pelo que não tem de implementar novas tentativas em caso de falha. No entanto, as transações entre grupos podem falhar devido a modificações simultâneas, pelo que devem ter novas tentativas. A confirmação e a reversão de uma transação só de leitura são ambas operações nulas.

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

Colocação em fila de tarefas transacionais

Pode colocar uma tarefa em fila como parte de uma transação do Datastore, de modo que a tarefa só seja colocada em fila (e seja garantido que é colocada em fila) se a transação for confirmada com êxito. Se a transação for confirmada, a tarefa é garantidamente colocada em fila. Uma vez colocada em fila, não é garantido que a tarefa seja executada imediatamente e todas as operações realizadas na tarefa são executadas independentemente da transação original. A tarefa é repetida até ser bem-sucedida. Isto aplica-se a qualquer tarefa colocada na fila no contexto de uma transação.

As tarefas transacionais são úteis porque permitem inscrever ações que não sejam do Datastore numa transação do Datastore (como enviar um email para confirmar uma compra). Também pode associar ações do Datastore à transação, como confirmar alterações a grupos de entidades adicionais fora da transação, se e apenas se a transação for bem-sucedida.

Uma aplicação não pode inserir mais de cinco tarefas transacionais em filas de tarefas durante uma única transação. As tarefas transacionais não podem ter nomes especificados pelo utilizador.

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

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

// ...

txn.commit();