Transações

Observação: os desenvolvedores que criam novos aplicativos são bastante incentivados a usar a biblioteca de cliente do NDB, que oferece diversos benefícios em comparação a 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 de DB, leia o Guia de migração de DB para NDB

O Cloud Datastore aceita 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.

Um aplicativo executa um conjunto de instruções e operações de armazenamento de dados em uma única transação. Assim, caso alguma instrução ou operação gere uma exceção, nenhuma operação do Cloud Datastore no conjunto será aplicada. O aplicativo define as ações a serem executadas na transação usando uma função do Python. A transação é iniciada pelo aplicativo por meio de um dos métodos run_in_transaction dependendo se ela acessa entidades em único grupo de entidades ou se é uma transação entre grupos.

Para o caso de uso comum de uma função usada apenas nas 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 for chamada sem uma transação, em vez de decorá-la, chame db.run_in_transaction() usando a função como 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() recebe o objeto da função e os argumentos posicionais e de palavras-chave para transferir para a função. Quando a função retorna um valor, a db.run_in_transaction() retorna esse valor.

Se a função retornar, a transação será confirmada e todos os efeitos das operações do Cloud 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, o padrão de comportamento de @db.transactional e db.run_in_transaction() são diferentes. O @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.

Usando @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()

Usando 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 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, é 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 Cloud Datastore. A função de transação é chamada várias vezes no caso de falha em uma operação do Cloud Datastore, devido à atualização simultânea de entidades por outro usuário, no grupo de entidades. Quando isso acontece, a Cloud Datastore API repete a transação um número fixo de vezes. No caso de todos falharem, a db.run_in_transaction() gera um TransactionFailedError. É possível ajustar a quantidade de repetições da transação 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, caso uma nova entidade do Cloud Datastore seja armazenada pela transação, o código da entidade criado será salvo para uso futuro. Em caso de falha, o código salvo não terá relação com a 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 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 página da Wikipédia sobre isolamento serializável (em inglês) 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 respectivo 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 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 é 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, 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.

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 Cloud 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 é aplicado 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 Cloud 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 Cloud 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, ....)
Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…

Ambiente padrão do App Engine para Python 2