Conţinut
Aceasta este pentru aplicațiile care rulează Django cu PSQL sau alte baze de date (DB-uri) bazate pe SQL (SQLite, MySQL etc.); este posibil ca regulile care stau la baza tranzacțiilor să nu se aplice altor DB. Sub capotă, codul de gestionare a tranzacțiilor Django face următoarele (acesta este copiat din documentul lui Django -defilați în jos- cu propriile mele puncte pentru clarificare):
Obține sau creează o conexiune la DB și o setează ca tranzacție.
Toate operațiunile DB necesită blocări — unele blocaje sunt libere și vor permite altor conexiuni să execute operațiuni pe aceeași resursă, dar alte blocări sunt mai stricte; sunt exclusive și vor bloca atât operațiile de citire, cât și de scriere. Crearea, actualizarea și ștergerea înregistrărilor, precum și modificarea tabelelor și coloanelor, vor obține blocări mai stricte care vor bloca alte conexiuni de la executarea operațiunilor. Aceste blocări sunt păstrate până la sfârșitul tranzacției.
Acest lucru se face automat atunci când, de exemplu, se inserează înregistrări într-un tabel - unele operații de model se vor încheia automat în blocuri atomice.
Acest lucru se poate face manual apelând o funcție care este înfășurată de atomic() sau folosind atomic() într-o instrucțiune with (ca manager de context).
Punctele de salvare vor folosi aceeași conexiune ca și tranzacția exterioară - dacă vă uitați la codul Django pentru punctele de salvare, este doar un apel recursiv pentru a crea o altă tranzacție.
Punctele de salvare pot fi imbricate în punctele de salvare — o stivă ține evidența punctelor de salvare.
După inserare, punctul de salvare este eliminat fie prin comiterea modificărilor și punerea lor la dispoziție pentru restul tranzacției (dar NU alte conexiuni), fie prin anularea modificărilor.
Blocările de pe DB sunt încă ținute în timp ce logica non-DB este executată, de exemplu, dacă trimitem o cerere HTTP în corpul unei tranzacții, blocările vor fi reținute până când acea cerere este trimisă și răspunsul este returnat. Django nu eliberează blocările atunci când oprim executarea operațiunilor DB. Acesta deține încuietori PÂNĂ când tranzacția exterioară este încheiată.
Blocările dobândite în tranzacțiile imbricate sunt, de asemenea, păstrate până când tranzacția exterioară este încheiată sau tranzacția imbricată este anulată.
Tranzacția este oprită, derulată înapoi și blocările sunt eliberate.
Dacă eroarea apare într-o tranzacție imbricată, tranzacția exterioară nu este afectată. Are loc rollback-ul, dar tranzacția externă poate continua neîntreruptă (atâta timp cât eroarea generată este prinsă și gestionată).
Tranzacțiile nu detectează erori, dar depind de acestea pentru a ști când să anuleze modificările. Prin urmare, nu împachetați operațiunile DB într-un try/except, în schimb, împachetați-le într-o tranzacție și apoi împachetați tranzacția într-un try/except. Procedând astfel, operațiunea DB eșuează și va fi anulată automat, fără a afecta tranzacția externă.
În cele din urmă, întreaga tranzacție este comisă, făcând modificările disponibile tuturor utilizatorilor/conexiunilor DB. Sau este derulat înapoi, lăsând DB în aceeași stare în care era atunci când a fost întâlnită tranzacția.
În următorul blog, voi scrie despre ce ar trebui sau nu ar trebui să puneți într-o tranzacție pentru a evita întreruperile cauzate de înfometarea lacătelor.
Tranzacțiile sunt mecanisme care permit DB-urilor ACID (PSQL, SQL, cel mai recent MongoDB etc.) să fie ATOMIC (A în ACID). Aceasta înseamnă că toate scrierile în DB sunt efectuate ca o singură operațiune – indiferent cât de complexă. Dacă scrierea reușește, toate modificările din DB persistă și sunt disponibile pentru toate conexiunile simultan (fără scrieri parțiale). Dacă scrierea eșuează, toate modificările sunt anulate - din nou, nu există modificări parțiale.
Tranzacțiile garantează următoarele reguli:
Aceste reguli permit utilizatorului DB să grupeze operațiuni complexe și să le execute ca o singură operație. De exemplu, executarea unui transfer dintr-un cont bancar în altul; dacă nu am avut tranzacții, atunci în mijlocul acestor două operațiuni de scriere ar putea exista o retragere sau chiar o operațiune de închidere a contului care face transferul invalid. Tranzacțiile permit utilizatorului o anumită formă de control al traficului. Putem bloca toate celelalte operațiuni conflictuale în timp ce tranzacția este în curs.
Operațiunile sunt blocate printr-o serie de încuietori pe tabele și rânduri. În PSQL și alte variante SQL, tranzacțiile sunt create cu BEGIN; atunci blocările sunt achiziționate atunci când se execută o operațiune precum select/insert/delete/alter și tranzacțiile se termină cu COMMIT sau ROLLBACK. Blocările sunt eliberate când se execută COMMIT sau ROLLBACK. Din fericire, Django ne permite să creăm tranzacții fără a fi nevoie să folosim aceste trei declarații (dar trebuie să ne îngrijorăm totuși cu privire la blocaje; mai multe despre asta în postarea următoare).
-- 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 împachetează automat fiecare operațiune DB într-o tranzacție pentru noi atunci când rulează în modul autocommit (mai multe despre asta mai jos). Tranzacțiile explicite pot fi create folosind decoratorul atomic() sau managerul de context (cu atomic()) — atunci când este utilizat atomic(), operațiunile DB individuale NU sunt incluse într-o tranzacție sau trimise imediat în DB (sunt executate, dar modificările în DB nu sunt vizibile pentru alți utilizatori). În schimb, întreaga funcție este înfășurată într-un bloc de tranzacție și comisă la sfârșit (dacă nu sunt generate erori) — COMMIT este executat.
Dacă există erori, modificările DB sunt anulate — ROLLBACK este executat. Acesta este motivul pentru care tranzacțiile păstrează încuietori până la sfârșit; nu vrem să eliberăm o blocare pe o masă, să facem ca o altă conexiune să schimbe tabelul sau să-l citim și apoi să fim forțați să derulăm înapoi ceea ce tocmai a fost schimbat sau citit.
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()
De asemenea, putem imbrica tranzacții apelând o altă funcție care este încapsulată de atomic() sau folosind managerul de context în cadrul unei tranzacții. Acest lucru ne permite să creăm puncte de salvare în care putem încerca operațiuni riscante fără a afecta restul tranzacției - atunci când este detectată o tranzacție internă, este creat un punct de salvare, tranzacția internă este executată și, dacă tranzacția internă eșuează, datele sunt returnate la punctul de salvare și tranzacția externă continuă. Dacă tranzacția externă eșuează, toate tranzacțiile interioare sunt anulate împreună cu tranzacția externă. Imbricarea tranzacțiilor poate fi prevenită prin setarea durable=True în atomic() - acest lucru va face ca tranzacția să genereze o RuntimeError dacă sunt detectate tranzacții interne.
Ce trebuie să rețineți despre tranzacțiile imbricate:
DATABASES = { 'default': { # True by default 'AUTOCOMMIT': False, # ...rest of configs } }
În mod implicit, Django rulează în modul autocommit , ceea ce înseamnă că fiecare operație DB este încapsulată într-o tranzacție și rulează imediat (committed, deci autocommit), cu excepția cazului în care operația este în mod explicit inclusă într-o tranzacție de către utilizator. Acest lucru obstrucționează logica tranzacției a DB-ului de bază - unele DB, cum ar fi PSQL, înglobează automat toate operațiunile în tranzacții, în timp ce altele nu. Dacă îl avem pe Django care face asta pentru noi, nu trebuie să ne facem griji cu privire la logica DB de bază.
Putem dezactiva modul autocommit, dar asta ne-ar face să ne bazăm pe DB pentru a gestiona operațiunile și a ne asigura că sunt ATOMIC, ceea ce este riscant și incomod pentru dezvoltatori. Nu ar fi distractiv dacă, în timpul creării unui API, ar trebui să ne dăm seama care operațiuni de scriere sunt în tranzacții și, prin urmare, vor fi scrise în totalitate și nu parțial.
Cu toate acestea, este posibil să dorim să dezactivăm modul autocommit dacă știm numărul de conexiuni pe DB și secvența operațiunilor pe care le execută. Dacă ne dăm seama cum să deconflictăm aceste operațiuni, atunci putem dezactiva modul autocommit și obținem câteva eficiențe:
Dezavantajele sunt riscurile mai mari de corupere a datelor. Nu merită, dar poate că există un caz de utilizare la care nu mă pot gândi.