In een app voor het genereren van dienstroosters heb ik in twee weken het schema drie keer incompatibel gewijzigd. Ik maakte een tabel worker_verifications om het resultaat te bewaren van de controles die het model per medewerker deed. Vijf dagen later gooide ik hem helemaal weg en zette ik de enige bruikbare kolom, checked_rules, in worker_week_rules. Twee dagen daarna verwijderde ik extra_closed_days uit de tabel weeks, die ik bij het vorige herontwerp had toegevoegd.
Geen van die wijzigingen kwam door een nieuwe productbeslissing. Ze kwamen doordat ik het domein beter ging begrijpen.
Het schema van dag 1 en dat van dag 30
De tabel die ik weggooide had acht kolommen: ai_result, worker_hash, context_hash, verified_at, checked_rules en de sleutels. Het idee was de verificatie cachen: veranderden de hash van de medewerker en die van de context niet, dan riep je het model niet opnieuw aan. Klinkt redelijk.
Wat er gebeurde is dat de echte eenheid van het domein niet "een verificatie" was, maar "de regels van een medewerker in een week". En die rij bestond al. De verificaties waren een attribuut van die rij, geen entiteit. De migratie die het repareerde is kort en leest vanzelf:
ALTER TABLE "worker_week_rules" ADD COLUMN "checked_rules" text DEFAULT '{}' NOT NULL;
UPDATE "worker_week_rules" wwr
SET "checked_rules" = wv."checked_rules"
FROM "worker_verifications" wv
WHERE wwr."week_id" = wv."week_id"
AND wwr."worker_id" = wv."worker_id"
AND wv."checked_rules" != '{}';
DROP TABLE "worker_verifications";
ALTER TABLE "worker_week_rules" DROP COLUMN "created_at";
Vier statements. De kosten zaten niet in het SQL.
Welke wijzigingen een teken zijn dat je het goed had en welke niet
Er zijn twee soorten schemawijzigingen en ze lijken totaal niet op elkaar.
- Een kolom toevoegen.
checked_week_rulesinweeks,calendar_rulesincalendars. Het is eenALTER TABLE ADD COLUMN ... DEFAULTmet een default, je deployt zonder iets stil te leggen, de oude code blijft werken. Dit is het systeem dat groeit. Goed teken. - De entiteit verplaatsen. Een tabel verwijderen, een kolom hernoemen, veranderen waar een gegeven onder hangt. Dat betekent dat je het domein de eerste keer verkeerd hebt gemodelleerd, niet dat het domein veranderd is.
In datzelfde project veranderde ik preferred_day_off integer in preferred_days_off text, want het bleek dat een medewerker twee dagen kon prefereren. Dat is ook een goede: het is nieuwe informatie over de wereld, geen modelleerfout. De migratie past in drie regels en behoudt de data met een CAST.
Die van het slechte soort herken je doordat de diff niet in de migratiemap zit.
De echte kosten zitten in de UI, niet in het SQL
In een documentatietool met AI noemde ik wat de gebruiker uploadde eerst documents. Een maand later was het juiste concept contexts: stukken tekst die de agent kon lezen, citeren en herschrijven, en die uit een URL konden komen, niet alleen uit een bestand. De naam was fout en trok het hele model mee.
De commit die dat veranderde raakt het schema niet. Hij raakt de chatpagina, de input, en verwijdert context-url-dialog, context-view en context-viewer compleet: 1.127 regels in één klap weg. Een RENAME COLUMN van twee seconden en een hele dag componenten aanpassen.
En een geval dat nog goedkoper is om uit te voeren en nog duurder om door te voeren: in datzelfde project hernoemde ik starCount naar favoriteCount. Het SQL is vijf regels in twee migraties. De diff raakt het schema, twee bestanden met database-acties, en de UI die het toonde.
Dat is de regel die ik eruit meenam: de kosten van het hernoemen van een kolom zijn evenredig met het aantal componenten dat de naam letterlijk uitschrijft. Niet met wat de migratie kost.
Hoe ik vandaag het eerste schema schrijf
Vier dingen die ik sindsdien anders doe, en geen ervan is "beter ontwerpen".
- Ik benoem naar wat iets is, niet naar waar het vandaan komt.
documentswas de herkomst (een bestand).contextsis de rol die het gegeven in het systeem speelt. De tweede naam overleeft een verandering van herkomst. - Ik maak geen tabel voor één attribuut. Als de nieuwe entiteit altijd precies één rij heeft per rij van een andere tabel, is het geen entiteit. Het is een groep kolommen.
worker_verificationswas dat, en ik had vijf dagen nodig om het te zien. - Vanaf het begin één type per kolom. In het grote herontwerp zette ik tegelijk
created_atvantextnaartimestamptz,week_startvantextnaardateen de als tekst opgeslagen JSON naarjsonb. Dat had ik allemaal op dag één voor dezelfde prijs goed kunnen doen. Achteraf repareren is eenALTER COLUMN ... USINGin elke tabel plus alle code nalopen die datums als strings vergeleek. - De UI herhaalt de kolomnaam niet. Een type afgeleid van het schema en een paar toegangsfuncties. Dan vangt de compiler een
RENAME COLUMNop in plaats van eengrep.
Wanneer stilstaan wél loont
In een editor voor kantoorplattegronden ging ik van aparte componenten DualDesk en QuadDesk naar één MultiDesk met een aantal plekken, en daarna refactorde ik het nog eens om uitgeschakelde plekken toe te voegen. Een bureau van twee en een van vier lijken twee gevallen; in werkelijkheid waren het dezelfde met een parameter. Dat zie je wél op dag één als je tien minuten stilstaat bij de vraag of die 2 en die 4 constanten zijn of data.
De regel die ik hanteer: als het verschil tussen twee modellen een getal is, is het data. Dáár loont het om eerst na te denken.
Voor de rest schrijf ik het schema in de wetenschap dat het fout is. Wat ik optimaliseer is niet dat ik het goed heb, maar dat het volgende week repareren een korte migratie kost en geen dag componenten aanpassen.
