Contente
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):
Obtém ou cria uma conexão com o banco de dados e a define como uma transação.
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.
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.
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.
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.
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.
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.
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:
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;
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:
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:
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.