Writing/Postgres
Un rând care ține de doi oameni, o ștergere care ține de unul
Orice aplicație ajunge la un moment dat să aibă un buton de ștergere a contului, de obicei pentru că îl cere un magazin. Într-o aplicație în care un rând descrie doi oameni, butonul ăla e o problemă de modelare a datelor deghizată în setare.
Ia un rând care înregistrează împărțirea bacșișului la finalul unei ture într-un restaurant. Un om a făcut împărțirea. Alt om a câștigat banii. Ambele fapte stau în același rând, în două coloane diferite, iar forma asta e cea care face ștergerea contului mult mai grea decât pare.
Implementarea evidentă șterge fiecare rând în care utilizatorul care pleacă apare în oricare dintre cele două coloane. Prima jumătate e corectă. A doua jumătate intră în înregistrări care aparțin tuturor celor pe care i-a plătit vreodată.
-- before: both sides of a two-party recordDELETE FROM public.tipout_results WHERE recipient_id = uid OR calculated_by = uid; -- other people's rows -- after: only what the departing user earned.-- calculated_by already had ON DELETE SET NULL,-- so the schema had answered this all along.DELETE FROM public.tipout_results WHERE recipient_id = uid;Așa că un ospătar-șef care a făcut împărțirea în fiecare vineri timp de un an ia cu el, la plecare, un an din înregistrările tuturor celorlalți. Oamenii ăia nu au fost întrebați niciodată și nu ar afla niciodată, iar ce pierd e evidența turelor pe care chiar le-au lucrat, adică exact evidența după care ar întinde mâna la declarația de venit.
De ce era clauza acolo de la bun început
Asta e partea care valorează mai mult decât bugul în sine, pentru că o instrucțiune ca aia rareori e neatenție de moment. De obicei e acolo ca să acopere o contradicție care stă deja în schemă.
Cheia străină de pe partea celui care calculează declară ON DELETE SET NULL. Asta e schema răspunzând deja la întrebare: împărțirea supraviețuiește, iar referința către autor se golește. Dar dacă acea coloană e și NOT NULL, cele două nu pot fi adevărate amândouă, iar Postgres dă eroare în clipa în care dispare profilul. Ștergerea rândurilor mai întâi face eroarea să dispară, iar de aceea contradicția nu iese niciodată la iveală ca defecțiune. Iese la iveală ca date lipsă la altcineva.
“O ștergere scrisă ca să rezolve o ambiguitate e semnul că ambiguitatea e încă în schemă.”
Soluția și ce merită generalizat din ea
Ștergerea doar a rândurilor în care utilizatorul care pleacă e beneficiarul e toată soluția. Câștigurile lui dispar. Împărțirile pe care le-a făcut pentru alții rămân, cu autorul golit, exact cum spunea cheia străină dintotdeauna.
Există o metodă de a dovedi asta pe date reale fără să fii curajos: o tranzacție care se anulează singură. Șterge un profil, verifică faptul că rândul celeilalte părți a supraviețuit și că referința către autor a fost golită, apoi dă totul înapoi și confirmă că numărul de rânduri e neschimbat. E singura metodă în care am încredere pentru o migrare distructivă, pentru că o bază de date de test nu conține niciodată rândul care te surprinde.
Generalizarea e despre ștergere în orice aplicație în care înregistrările descriu relații, nu posesiuni. Întrebarea nu e ce aparține acestui utilizator. E ce ar fi distrus dacă rândul ăsta dispare și dacă toți cei afectați au fost de acord.
Orice aplicație ajunge la butonul ăsta, de obicei pentru că îl cere un magazin. Merită tratat ca un exercițiu de modelare a datelor, nu ca un rând într-un ecran de setări, pentru că e singura operație din care nu te poți scuza după aceea. Verificarea e rapidă: pentru fiecare tabel pe care îl atinge ștergerea, numără id-urile de utilizator dintr-un rând. Dacă e mai mult de unul, decizi în numele cuiva care nu e în cameră.
