Nota: os programadores que criam novas aplicações são fortemente aconselhados a usar a biblioteca de cliente NDB, que tem várias vantagens em comparação com esta biblioteca de cliente, como o armazenamento em cache automático de entidades através da API Memcache. Se estiver a usar atualmente a biblioteca cliente DB mais antiga, leia o guia de migração de DB para NDB
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.
Uma aplicação pode executar um conjunto de declarações e operações de armazenamento de dados numa única transação, de modo que, se alguma declaração ou operação gerar uma exceção, nenhuma das operações do Datastore no conjunto é aplicada. A aplicação define as ações a realizar na transação através de uma função Python. A aplicação inicia a transação através de um dos métodos run_in_transaction
, consoante a transação aceda a entidades num único grupo de entidades ou seja uma transação entre grupos.
Para o exemplo de utilização comum de uma função que só é usada 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 for chamada por vezes sem uma transação, em vez de a decorar, chame db.run_in_transaction()
com 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 de função e os argumentos posicionais e de palavras-chave a transmitir à função. Se a função devolver um valor,
db.run_in_transaction()
devolve esse valor.
Se a função for devolvida, a transação é confirmada e todos os efeitos das operações do Datastore são aplicados. Se a função gerar uma exceção, a transação é "revertida" e os efeitos não são aplicados. Consulte a nota acima sobre exceções.
Quando uma função de transação é chamada a partir de outra transação, @db.transactional
e db.run_in_transaction()
têm um comportamento predefinido diferente. @db.transactional
permite esta situação, e a transação interna torna-se a mesma transação que a transação externa. A chamada db.run_in_transaction()
tenta "anichar" outra transação na transação existente, mas este comportamento ainda não é suportado e gera db.BadRequestError
. Pode especificar outro comportamento. Consulte a referência da função nas opções de transação para ver detalhes.
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 se o código tentar atualizar entidades de mais do que um grupo de entidades. Para invocar uma transação entre grupos, use as opções de transação.
Usar o @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()
Usar o 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 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.
Todo o outro código Python é permitido numa função de transação. Pode determinar se o âmbito atual está aninhado numa função de transação através de db.is_in_transaction()
.
A função de transação não deve ter efeitos secundários além das operações da base de dados. A função de transação pode ser chamada várias vezes se uma operação do Datastore falhar devido a outro utilizador que esteja a atualizar entidades no grupo de entidades ao mesmo tempo. Quando isto acontece, a API Datastore tenta novamente a transação um número fixo de vezes. Se todas falharem, o db.run_in_transaction()
gera um
TransactionFailedError
.
Pode ajustar o número de vezes que a transação é repetida usando
db.run_in_transaction_custom_retries()
em vez de db.run_in_transaction().
Da mesma forma, a função de transação não deve ter efeitos secundários que dependam do êxito da transação, a menos que o código que chama a função de transação saiba como anular esses efeitos. Por exemplo, se a transação armazenar uma nova entidade do Datastore, guardar o ID da entidade criada para utilização posterior e, em seguida, a transação falhar, o ID guardado não se refere à entidade pretendida porque a criação da entidade foi revertida. Neste caso, o código de chamada tem de ter cuidado para não usar o ID guardado.
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.
def increment_counter(key, amount):
obj = db.get(key)
obj.counter += amount
obj.put()
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 é repetida até que todos os passos sejam concluídos sem interrupção.
Outra utilização comum das transações é obter uma entidade com uma chave com nome ou criá-la se ainda não existir:
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
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 volta a tentar, repara que a entidade já existe e atualiza a entidade.
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.
A função get-or-create é tão útil que existe um método incorporado para a mesma:
Model.get_or_insert()
usa um nome de chave, um elemento principal opcional e argumentos para transmitir ao construtor do modelo se não existir uma entidade com esse nome e caminho. A tentativa de obtenção e a criação ocorrem numa transação, pelo que (se a transação for bem-sucedida) o método devolve sempre uma instância do modelo que representa uma entidade real.
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.
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
Colocação em fila de tarefas transacionais
Pode colocar uma tarefa em fila como parte de uma transação do Datastore, para que a tarefa só seja colocada em fila se a transação for confirmada com êxito. Se
a transação não for confirmada, a tarefa não é colocada na fila. Se a transação for confirmada, a tarefa é colocada em fila. Depois de adicionada à fila, a tarefa não é executada imediatamente, pelo que não é atómica com a transação.
No entanto, depois de adicionada à fila, a tarefa é repetida até ser bem-sucedida. Isto aplica-se a qualquer tarefa colocada em fila 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 numa transação que depende do êxito da transação (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 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.
def do_something_in_transaction(...)
taskqueue.add(url='/path/to/my/worker', transactional=True)
...
db.run_in_transaction(do_something_in_transaction, ....)