Contenuto
Questo vale per le app che eseguono Django con PSQL o altri database (DB) simili basati su SQL (SQLite, MySQL, ecc.); le regole di base sulle transazioni potrebbero non essere valide per altri DB. In realtà, il codice di gestione delle transazioni di Django fa quanto segue (copiato dalla documentazione di Django - scorri verso il basso - con i miei punti elenco per chiarimenti):
Ottiene o crea una connessione al DB e la imposta come transazione.
Tutte le operazioni DB richiedono blocchi: alcuni blocchi sono allentati e consentiranno ad altre connessioni di eseguire operazioni sulla stessa risorsa, ma altri blocchi sono più rigidi; sono esclusivi e bloccheranno sia le operazioni di lettura che di scrittura. La creazione, l'aggiornamento e l'eliminazione di record, così come la modifica di tabelle e colonne, acquisiranno blocchi più rigidi che impediranno ad altre connessioni di eseguire operazioni. Questi blocchi vengono mantenuti fino alla FINE della transazione.
Ciò avviene automaticamente quando, ad esempio, si inseriscono record in una tabella: alcune operazioni del modello si racchiudono automaticamente in blocchi atomici.
È possibile farlo manualmente chiamando una funzione racchiusa da atomic() o utilizzando atomic() in un'istruzione with (come gestore del contesto).
I punti di salvataggio utilizzeranno la stessa connessione della transazione esterna: se si esamina il codice Django per i punti di salvataggio, si tratta semplicemente di una chiamata ricorsiva per creare un'altra transazione.
I punti di salvataggio possono essere annidati all'interno di altri punti di salvataggio: una pila tiene traccia dei punti di salvataggio.
Dopo l'inserimento, il punto di salvataggio viene rimosso o confermando le modifiche e rendendole disponibili al resto della transazione (ma NON ad altre connessioni) o annullando le modifiche.
I blocchi sul DB sono ancora mantenuti mentre viene eseguita la logica non DB, ad esempio, se inviamo una richiesta HTTP nel corpo di una transazione, i blocchi saranno mantenuti finché la richiesta non viene inviata e la risposta non viene restituita. Django non rilascia i blocchi quando interrompiamo l'esecuzione delle operazioni DB. Mantiene i blocchi FINO al termine della transazione esterna.
Anche i blocchi acquisiti nelle transazioni nidificate vengono mantenuti fino al completamento della transazione esterna o al rollback della transazione nidificata.
La transazione viene interrotta, annullata e i blocchi vengono rilasciati.
Se l'errore si verifica in una transazione nidificata, la transazione esterna non viene influenzata. Il rollback avviene ma la transazione esterna può continuare senza interruzioni (finché l'errore sollevato viene intercettato e gestito).
Le transazioni non rilevano errori, ma dipendono da essi per sapere quando effettuare il rollback delle modifiche. Pertanto, non avvolgere le operazioni DB in un try/except, ma piuttosto avvolgerle in una transazione e quindi avvolgere la transazione in un try/except. In questo modo, l'operazione DB fallisce e viene automaticamente annullata senza influire sulla transazione esterna.
Infine, l'intera transazione viene confermata, rendendo le modifiche disponibili a tutti gli utenti/connessioni DB. Oppure viene annullata, lasciando il DB nello stesso stato in cui si trovava quando è stata rilevata la transazione.
Nel prossimo blog scriverò su cosa si dovrebbe o non si dovrebbe inserire in una transazione per evitare interruzioni causate dalla mancanza di blocchi.
Le transazioni sono meccanismi che consentono ai DB ACID (PSQL, SQL, l'ultimo MongoDB , ecc.) di essere ATOMIC (la A in ACID). Ciò significa che tutte le scritture sul DB vengono eseguite come un'unica operazione, indipendentemente dalla loro complessità. Se la scrittura riesce, tutte le modifiche al DB persistono e sono disponibili per tutte le connessioni contemporaneamente (nessuna scrittura parziale). Se la scrittura fallisce, tutte le modifiche vengono annullate, di nuovo, non ci sono modifiche parziali.
Le transazioni garantiscono queste regole:
Queste regole consentono all'utente del DB di raggruppare operazioni complesse ed eseguirle come un'unica operazione. Ad esempio, eseguire un trasferimento da un conto bancario a un altro; se non avessimo transazioni, allora nel mezzo di queste due operazioni di scrittura, potrebbe esserci un prelievo o persino un'operazione di chiusura del conto che rende il trasferimento non valido. Le transazioni consentono all'utente una qualche forma di controllo del traffico. Possiamo bloccare tutte le altre operazioni in conflitto mentre la transazione è in corso.
Le operazioni vengono bloccate tramite una serie di blocchi su tabelle e righe. In PSQL e altre varianti di SQL, le transazioni vengono create con BEGIN; quindi i blocchi vengono acquisiti quando viene eseguita un'operazione come select/insert/delete/alter e le transazioni terminano con COMMIT o ROLLBACK. I blocchi vengono rilasciati quando viene eseguito COMMIT o ROLLBACK. Fortunatamente, Django ci consente di creare transazioni senza dover usare queste tre istruzioni (ma dobbiamo comunque preoccuparci dei blocchi; ne parleremo più in dettaglio nel prossimo 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()
Django avvolge automaticamente ogni operazione DB in una transazione per noi quando viene eseguito in modalità autocommit (maggiori dettagli di seguito). Le transazioni esplicite possono essere create usando il decoratore atomic() o il context manager (con atomic()) — quando si usa atomic(), le singole operazioni DB NON vengono avvolte in una transazione o impegnate immediatamente nel DB (vengono eseguite, ma le modifiche al DB non sono visibili ad altri utenti). Invece, l'intera funzione viene avvolta in un blocco di transazione e impegnata alla fine (se non vengono sollevati errori) — viene eseguito COMMIT.
Se ci sono errori, le modifiche al DB vengono annullate: viene eseguito ROLLBACK. Ecco perché le transazioni mantengono i blocchi fino alla fine; non vogliamo rilasciare un blocco su una tabella, far sì che un'altra connessione modifichi la tabella o la legga e poi essere costretti a annullare ciò che è stato appena modificato o letto.
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()
Possiamo anche nidificare le transazioni chiamando un'altra funzione che è racchiusa da atomic() o usando il gestore di contesto all'interno di una transazione. Questo ci consente di creare punti di salvataggio in cui possiamo tentare operazioni rischiose senza influenzare il resto della transazione: quando viene rilevata una transazione interna, viene creato un punto di salvataggio, la transazione interna viene eseguita e, se la transazione interna fallisce, i dati vengono riportati al punto di salvataggio e la transazione esterna continua. Se la transazione esterna fallisce, tutte le transazioni interne vengono riportate indietro insieme alla transazione esterna. L'annidamento delle transazioni può essere impedito impostando Durable=True in atomic(): questo farà sì che la transazione sollevi un RuntimeError se vengono rilevate delle transazioni interne.
Cosa bisogna ricordare sulle transazioni nidificate:
DATABASES = { 'default': { # True by default 'AUTOCOMMIT': False, # ...rest of configs } }
Di default, Django funziona in modalità autocommit , il che significa che ogni operazione DB è racchiusa in una transazione e immediatamente eseguita (commit, quindi autocommit) a meno che l'operazione non sia esplicitamente racchiusa in una transazione dall'utente. Questo offusca la logica di transazione del DB sottostante: alcuni DB, come PSQL, racchiudono automaticamente tutte le operazioni in transazioni, mentre altri no. Se facciamo fare questo a Django, non dobbiamo preoccuparci della logica del DB sottostante.
Possiamo disattivare la modalità autocommit, ma questo ci lascerebbe affidati al DB per gestire le operazioni e garantire che siano ATOMIC, il che è rischioso e scomodo per gli sviluppatori. Non sarebbe divertente se, durante la creazione di un'API, dovessimo capire quali operazioni di scrittura sono nelle transazioni e, pertanto, saranno scritte in totale e non in parte.
Tuttavia, potremmo voler disattivare la modalità autocommit se conosciamo il numero di connessioni sul DB e la sequenza di operazioni che stanno eseguendo. Se scopriamo come de-confliggere queste operazioni, allora possiamo disattivare la modalità autocommit e ottenere un po' di efficienza:
Gli svantaggi sono i rischi più elevati di corruzione dei dati. Non ne vale la pena, ma forse c'è un caso d'uso a cui non riesco a pensare.