paint-brush
Python: Uma análise mais aprofundada de como as transações Django funcionampor@mta
134 leituras Novo histórico

Python: Uma análise mais aprofundada de como as transações Django funcionam

por Michael T. Andemeskel9m2025/03/02
Read on Terminal Reader

Muito longo; Para ler

O transaction.atomic() do Django pode ser usado para criar transações para realizar operações atômicas - operações de BD que são garantidas para todas executarem e serem salvas juntas ou falharem. O Django cria transações usando o mecanismo de transação do BD subjacente, ele executa o código na transação e, se nenhum erro ocorrer, ele confirma a transação no BD.
featured image - Python: Uma análise mais aprofundada de como as transações Django funcionam
Michael T. Andemeskel HackerNoon profile picture
0-item


Contente

  • TLDR — Resumo da lógica de transação do Django
  • Diagrama Lógico
  • Transações e Operações ATOMIC
  • Transações em Django
  • Modo de auto-commit do Django — Desativando transações automáticas
  • Fontes

TLDR — Resumo da lógica de transação do Django

Isto é para aplicativos que executam Django com PSQL ou outros bancos de dados (DBs) baseados em SQL (SQLite, MySQL, etc.) similares; as regras subjacentes em torno de transações podem não se aplicar a outros DBs. Por baixo dos panos, o código de gerenciamento de transações do Django faz o seguinte (isso é copiado da documentação do Django - role para baixo - com meus próprios marcadores para esclarecimento):


  • Abre uma transação ao entrar no bloco atômico mais externo;
    • Obtém ou cria uma conexão com o banco de dados e a define como uma transação.


  • Solicita e adquire bloqueios quando uma operação de banco de dados é encontrada;
    • Todas as operações de BD exigem bloqueios — alguns bloqueios são frouxos e permitirão que outras conexões executem operações no mesmo recurso, mas outros bloqueios são mais rigorosos; eles são exclusivos e bloquearão operações de leitura e gravação. Criar, atualizar e excluir registros, bem como alterar tabelas e colunas, adquirirá bloqueios mais rigorosos que bloquearão outras conexões de executar operações. Esses bloqueios são mantidos até o FIM da transação.


  • Cria um ponto de salvamento ao entrar em um bloco atômico interno;
    • Isso é feito automaticamente ao, por exemplo, inserir registros em uma tabela — algumas operações de modelo serão automaticamente encapsuladas em blocos atômicos.

    • Isso pode ser feito manualmente chamando uma função encapsulada por atomic() ou usando atomic() em uma instrução with (como um gerenciador de contexto).

    • Os pontos de salvamento usarão a mesma conexão que a transação externa — se você observar o código do Django para pontos de salvamento, verá que é apenas uma chamada recursiva para criar outra transação.

    • Os pontos de salvamento podem ser aninhados dentro de outros pontos de salvamento — uma pilha mantém o controle dos pontos de salvamento.


  • Libera ou reverte para o ponto de salvamento ao sair de um bloco interno;
    • Após a inserção, o ponto de salvamento é removido, seja confirmando as alterações e disponibilizando-as para o restante da transação (mas NÃO para outras conexões) ou revertendo as alterações.


  • Quando um código que não toca no BD é encontrado, o código é executado normalmente;
    • Os bloqueios no BD ainda são mantidos enquanto a lógica não-BD é executada, por exemplo, se enviarmos uma solicitação HTTP no corpo de uma transação, os bloqueios serão mantidos até que a solicitação seja enviada e a resposta seja retornada. O Django não libera os bloqueios quando paramos de executar operações de BD. Ele mantém os bloqueios ATÉ que a transação externa seja concluída.

    • Os bloqueios adquiridos em transações aninhadas também são mantidos até que a transação externa seja concluída ou a transação aninhada seja revertida.


  • Quando ocorre um RuntimeError ou um erro é gerado fora do BD (não relacionado às operações do BD);
    • A transação é interrompida, revertida e os bloqueios são liberados.

    • Se o erro ocorrer em uma transação aninhada, a transação externa não será afetada. O rollback ocorre, mas a transação externa pode continuar ininterrupta (desde que o erro levantado seja capturado e manipulado).

    • As transações não capturam erros, mas dependem deles para saber quando reverter as alterações. Portanto, não envolva as operações do BD em um try/except, em vez disso, envolva-as em uma transação e, em seguida, envolva a transação em um try/except. Fazer isso permite que a operação do BD falhe e seja revertida automaticamente sem impactar a transação externa.


  • Confirma ou reverte a transação ao sair do bloco mais externo.
    • Finalmente, a transação inteira é confirmada, tornando as alterações disponíveis para todos os usuários/conexões do BD. Ou ela é revertida, deixando o BD no mesmo estado em que estava quando a transação foi encontrada.


No próximo blog, escreverei sobre o que você deve ou não colocar em uma transação para evitar interrupções causadas por falta de bloqueio.

Diagrama Lógico

Diagrama lógico de transações Django — Completo

Transações e Operações ATOMIC

Transações são mecanismos que permitem que ACID DB's (PSQL, SQL, MongoDB mais recente, etc.) sejam ATOMIC (o A em ACID). Isso significa que todas as gravações no DB são feitas como uma única operação — não importa quão complexa. Se a gravação for bem-sucedida, todas as alterações no DB persistirão e estarão disponíveis para todas as conexões simultaneamente (sem gravações parciais). Se a gravação falhar, todas as alterações serão revertidas — novamente, não há alterações parciais.


As transações garantem estas regras:

  • Os dados no banco de dados não serão alterados no meio de uma transação.
  • Nenhuma outra conexão poderá ver as alterações feitas pela transação até que ela seja concluída.
  • O banco de dados terá todos os novos dados ou nenhum deles.


Essas regras permitem que o usuário do BD agrupe operações complexas e as execute como uma operação. Por exemplo, executar uma transferência de uma conta bancária para outra; se não tivéssemos transações, então, no meio dessas duas operações de gravação, poderia haver uma retirada ou até mesmo uma operação de fechamento de conta que torna a transferência inválida. As transações permitem ao usuário alguma forma de controle de tráfego. Podemos bloquear todas as outras operações conflitantes enquanto a transação estiver em andamento.


As operações são bloqueadas por uma série de bloqueios em tabelas e linhas. No PSQL e outras variantes de SQL, as transações são criadas com BEGIN; então os bloqueios são adquiridos quando uma operação como select/insert/delete/alter é executada e as transações terminam com COMMIT ou ROLLBACK. Os bloqueios são liberados quando COMMIT ou ROLLBACK é executado. Felizmente, o Django nos permite criar transações sem ter que usar essas três instruções (mas ainda precisamos nos preocupar com os bloqueios; mais sobre isso no próximo post).


 -- Start a new transaction BEGIN; SELECT … INSERT INTO … UPDATE … DELETE FROM … -- Save the changes COMMIT;


  • O select adquire bloqueios que bloqueiam atualizações nas linhas que estão sendo lidas.
  • A inserção adquire bloqueios que bloqueiam atualizações nas linhas que estão sendo criadas.
  • A atualização adquire bloqueios que bloqueiam atualizações nas linhas que estão sendo atualizadas.
  • A exclusão adquire bloqueios que bloqueiam atualizações nas linhas que estão sendo excluídas.
  • Não precisamos verificar explicitamente se há erros e chamar ROLLBACK — a transação faz isso por nós.
  • Se houvesse uma operação de alteração de tabela ali, ela bloquearia todas as leituras e gravações na tabela (mais sobre isso no próximo blog).

Transações em Django

 from django.db import transaction from app.core.models import Accounts, Fee, NotificationJob def do_migration(): overdrawn_accounts = Accounts.objects.filter(type='overdrawn') for acount in overdrawn_accounts: create_notification(acount) @transaction.atomic def create_notification(acount: Accounts): # $5 fee for overdraft - 500 because we never store money as float!!! recall = Fee.objects.create(acount=acount, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') acount.status = 'awaiting_payment' acount.save() def do_migration2(): overdawn_account = Accounts.objects.filter(type='overdrawn') for account in overdawn_account: with transaction.atomic(): recall = Fee.objects.create(acount=account, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') account.status = 'awaiting_payment' account.save()


O Django encapsula automaticamente cada operação do BD em uma transação para nós quando executado no modo autocommit (mais sobre isso abaixo). Transações explícitas podem ser criadas usando o decorador atomic() ou o gerenciador de contexto (com atomic()) — quando atomic() é usado, operações individuais do BD NÃO são encapsuladas em uma transação ou confirmadas imediatamente no BD (elas são executadas, mas as alterações no BD não são visíveis para outros usuários). Em vez disso, a função inteira é encapsulada em um bloco de transação e confirmada no final (se nenhum erro for levantado) — COMMIT é executado.


Se houver erros, as alterações do BD são revertidas — ROLLBACK é executado. É por isso que as transações mantêm os bloqueios até o final; não queremos liberar um bloqueio em uma tabela, ter outra conexão alterando a tabela ou lendo-a e, então, ser forçado a reverter o que acabou de ser alterado ou lido.


 ANGRY_CUSTOMER_THRESHOLD = 3 @transaction.atomic def create_notification(acount: Accounts): recall = Fee.objects.create(acount=acount, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') try: # when this completes successfully, the changes will be available to the outer transaction with transaction.atomic(): owner = acount.owner fees = Fee.objects.filter(owner=owner).count() if fees >= ANGRY_CUSTOMER_THRESHOLD: for fee in fees: fee.amount = 0 fee.save() owner.status = 'angry' owner.save() # as long as we catch the error, the outer transaction will not be rolled back except Exception as e: logger.error(f'Error while removings fees for account {acount.id}: {e}') acount.status = 'awaiting_payment' acount.save()


Também podemos aninhar transações chamando outra função que é encapsulada por atomic() ou usando o gerenciador de contexto dentro de uma transação. Isso nos permite criar pontos de salvamento onde podemos tentar operações arriscadas sem afetar o resto da transação — quando uma transação interna é detectada, um ponto de salvamento é criado, a transação interna é executada e, se a transação interna falhar, os dados são revertidos para o ponto de salvamento e a transação externa continua. Se a transação externa falhar, todas as transações internas são revertidas junto com a transação externa. O aninhamento de transações pode ser evitado definindo durable=True em atomic() — isso fará com que a transação gere um RuntimeError se quaisquer transações internas forem detectadas.


O que você precisa lembrar sobre transações aninhadas:

  • Transações aninhadas NÃO liberam bloqueios, a menos que as transações aninhadas sejam revertidas ou a transação externa seja confirmada ou revertida.
  • Transações aninhadas NÃO capturam erros de BD; elas simplesmente revertem as operações na transação que causaram o erro. Ainda precisamos capturar o erro de BD para que a transação externa continue.
  • As alterações feitas em transações aninhadas ficam disponíveis para a transação externa e transações aninhadas subsequentes.

Modo Django Autocommit — Desativando transações automáticas

 DATABASES = { 'default': { # True by default 'AUTOCOMMIT': False, # ...rest of configs } }

Por padrão, o Django roda no modo autocommit , o que significa que cada operação do BD é encapsulada em uma transação e executada imediatamente (commitada, portanto autocommit), exceto se a operação for explicitamente encapsulada em uma transação pelo usuário. Isso ofusca a lógica de transação do BD subjacente — alguns BDs, como o PSQL, encapsulam automaticamente todas as operações em transações, enquanto outros não. Se o Django fizer isso para nós, não precisamos nos preocupar com a lógica do BD subjacente.


Podemos desligar o modo autocommit, mas isso nos deixaria dependendo do BD para gerenciar operações e garantir que elas sejam ATÔMICAS, o que é arriscado e inconveniente para desenvolvedores. Não seria divertido se, ao criar uma API, tivéssemos que descobrir quais operações de gravação estão em transações e, portanto, serão gravadas no total e não parcialmente.


No entanto, podemos querer desligar o modo autocommit se soubermos o número de conexões no BD e a sequência de operações que elas estão executando. Se descobrirmos como desconflitar essas operações, podemos desligar o modo autocommit e ganhar algumas eficiências:

  • Removemos dois comandos de cada operação: BEGIN e COMMIT/ROLLBACK. Mas a maioria dos BDs encapsula automaticamente cada operação em uma transação, então isso pode ser inútil.
  • Os bloqueios não serão mantidos por muito tempo — assim que uma operação termina, os bloqueios que ela contém são liberados, em vez de esperar o término de toda a transação.
  • Deadlocks são menos prováveis — deadlocks ocorrem quando duas transações precisam de um bloqueio que a outra transação mantém. Isso deixa ambas as transações esperando a outra terminar, mas nenhuma consegue terminar. Deadlocks ainda podem ocorrer, no entanto. Algumas operações de BD farão várias gravações, por exemplo, atualizar uma coluna que tem um índice atualizará a coluna e o índice (duas gravações, pelo menos). Isso pode levar a deadlocks se outra conexão tentar operar nas linhas ou colunas afetadas.


As desvantagens são os riscos maiores de corrupção de dados. Não vale a pena, mas talvez haja um caso de uso que não consigo pensar.

Fontes