Transações

Observação: é altamente recomendável a desenvolvedores que criam novos aplicativos usar a biblioteca de cliente NDB, porque ela oferece diversos benefícios em comparação com esta biblioteca de cliente, como armazenamento em cache automático de entidades por meio da API Memcache. Se você estiver usando a antiga biblioteca de cliente DB, leia o Guia de migração de DB para NDB.

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.

Um aplicativo pode executar um conjunto de declarações e operações de armazenamento de dados em uma única transação, de tal maneira que se qualquer declaração ou operação emitir uma exceção, nenhuma das operações do Datastore presentes no conjunto será aplicada. O aplicativo define as ações a serem executadas na transação usando uma função do Python. O aplicativo inicia a transação usando um dos métodos run_in_transaction, dependendo se a transação acessa entidades em um único grupo de entidades ou se a transação é entre grupos.

Para o caso de uso comum de uma função usada apenas em transações, use o decorador @db.transactional:

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

@db.transactional
def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

increment_counter(acc.key(), 5)

Se a função às vezes for chamada sem uma transação, em vez de decorá-la, chame db.run_in_transaction() com a função como um argumento:

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

db.run_in_transaction(increment_counter, acc.key(), 5)

db.run_in_transaction() usa o objeto de função e argumentos posicionais e de palavra-chave para passar para a função. Se a função retornar um valor, db.run_in_transaction() retornará esse valor.

Se a função retornar, a transação será confirmada e todos os efeitos das operações do Datastore serão aplicados. Se a função gerar uma exceção, a transação será "revertida" e os efeitos não serão aplicados. Veja a observação acima sobre exceções.

Quando uma função de transação é chamada de dentro de outra transação, @db.transactional e db.run_in_transaction() têm um comportamento padrão diferente. @db.transactional permitirá isso e a transação interna terá a mesma forma que a transação externa. Chamar a db.run_in_transaction() é uma tentativa de "aninhar" outra transação dentro da atual, mas esse comportamento ainda não é compatível e gera um db.BadRequestError. É possível especificar outro comportamento. Para mais detalhes, veja a referência da função nas opções de transação.

Como usar transações entre grupos (XG)

As transações entre grupos, que operam em vários grupos de entidades, comportam-se como transações de grupo único, mas não falham quando o código tentar atualizar entidades em mais de um grupo de entidades. Para invocar uma transação entre grupos, use as opções de transação.

Utilizando @db.transactional:

from google.appengine.ext import db

@db.transactional(xg=True)
def make_things():
  thing1 = Thing(a=3)
  thing1.put()
  thing2 = Thing(a=7)
  thing2.put()

make_things()

Utilizando db.run_in_transaction_options:

from google.appengine.ext import db

xg_on = db.create_transaction_options(xg=True)

def my_txn():
    x = MyModel(a=3)
    x.put()
    y = MyModel(a=7)
    y.put()

db.run_in_transaction_options(xg_on, my_txn)

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, é possível criá-las na transação com nomes ou códigos de chave.

Qualquer outro código Python é permitido dentro de uma função de transação. É possível determinar se o escopo atual está aninhado em uma função de transação usando db.is_in_transaction(). A função de transação não tem outros efeitos colaterais a não ser as operações do Datastore. A função de transação é chamada várias vezes no caso de falha em uma operação do Datastore, devido à atualização simultânea de entidades por outro usuário, no grupo de entidades. Quando isso acontece, a API Datastore repete a transação um número fixo de vezes. Se todos eles falharem, db.run_in_transaction() gera um TransactionFailedError. É possível ajustar o número de vezes em que a transação é repetida usando db.run_in_transaction_custom_retries() em vez de db.run_in_transaction().

De modo semelhante, a função de transação não tem efeitos adversos que dependam do sucesso da transação, a menos que o código que chama a função da transação saiba como desfazer tais efeitos. Por exemplo, se a transação armazenar uma nova entidade do Datastore, salvar o código da entidade criada para uso posterior e depois a transação falhar, o código salvo não se referirá à entidade pretendida, porque a criação da entidade foi revertida. O código de chamada tem de ter o cuidado de não usar o código salvo neste caso.

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 seu valor atual.

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

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. Caso a entidade seja atualizada durante a transação, esta será repetida até que todas as etapas estejam concluídas sem interrupção.

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

class SalesAccount(db.Model):
    address = db.PostalAddressProperty()
    phone_number = db.PhoneNumberProperty()

def get_or_create(parent_key, account_id, address, phone_number):
    obj = db.get(db.Key.from_path("SalesAccount", account_id, parent=parent_key))
    if not obj:
        obj = SalesAccount(key_name=account_id,
                           parent=parent_key,
                           address=address,
                           phone_number=phone_number)
        obj.put()
    else:
        obj.address = address
        obj.phone_number = phone_number

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 é feita novamente, percebe que a entidade já existe e faz a atualização.

Quando uma transação falha, podem ser feitas novas tentativas até que ela seja bem-sucedida. Como alternativa, os usuários podem lidar com o erro por meio da propagação dela 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.

"Get-or-create" é tão útil que existe um método integrado para isso: o Model.get_or_insert() recebe um nome de chave, um pai opcional e argumentos para transferir o construtor do modelo caso não exista uma entidade com esse nome e caminho. A tentativa de receber e a criação ocorrem em uma única transação, por isso, caso a transação seja bem-sucedida, o método sempre retornará uma instância do modelo que representará uma entidade real.

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.

class Customer(db.Model):
    user = db.StringProperty()

class Account(db.Model):
    """An Account has a Customer as its parent."""
    address = db.PostalAddressProperty()
    balance = db.FloatProperty()

def get_all_accounts():
    """Returns a consistent view of the current user's accounts."""
    accounts = []
    for customer in Customer.all().filter('user =', users.get_current_user().user_id()):
        accounts.extend(Account.all().ancestor(customer))
    return accounts

Enfileiramento de tarefa transacional

É possível enfileirar uma tarefa como parte de uma transação do Datastore, assim ela será enfileirada apenas caso a transação seja confirmada com êxito. Não sendo confirmada, a transação não será enfileirada. Em caso de confirmação, a tarefa será enfileirada. Ela não é executada imediatamente após ser enfileirada, por isso a tarefa não é atômica com a transação. Ainda assim, uma vez enfileirada, a tarefa tentará novamente até conseguir. Isso se aplica a qualquer tarefa enfileirada durante uma função run_in_transaction().

As tarefas transacionais são úteis porque permitem combinar ações que não são do Datastore com uma transação que depende do êxito da transação, como o envio de um e-mail para confirmar uma compra. É possível também vincular ações do Datastore à transação, como confirmar alterações em grupos de entidades fora dela, apenas em caso de êxito dessa transação.

Um aplicativo não consegue inserir mais de cinco tarefas transacionais em filas de tarefas durante uma única transação. As tarefas transacionais não têm nomes especificados pelo usuário.

def do_something_in_transaction(...)
    taskqueue.add(url='/path/to/my/worker', transactional=True)
  ...

db.run_in_transaction(do_something_in_transaction, ....)