Database-migrationer i teams (så du slipper for ‘det virker på min maskine’)
Jeg har engang trykket “kør migration” fredag eftermiddag på en delt database og tænkt: “Det går nok, det er jo bare en lille ændring”. Tre minutter senere sad en kollega og kunne ikke logge ind i sin lokale app, mine tests fejlede, og vi brugte en halv time på at gætte, hvem der havde kørt hvad, hvor og hvornår.
Fejlen? Ingen fælles arbejdsgang. Bare “den der lige er færdig, kører migrationen”. Det fungerer nogenlunde, lige indtil flere begynder at skifte på det samme schema.
I den her artikel bygger vi en simpel, men ret stram workflow omkring database migrations, som et helt team kan bruge uden at få ondt i maven hver gang nogen skal ændre en kolonne.
Hvorfor database migrations går galt i teams
De fleste problemer med database migrations handler ikke om værktøjet. De handler om mennesker, timing og mangel på aftaler.
Scenario 1: “Det virker på min DB”
En klassiker: Du laver en feature-branch, smider en migration på, kører den mod din lokale database, alt spiller. Du åbner en pull request, den bliver godkendt, I merger og deployer.
Men en anden udvikler har lavet en anden migration, i en anden branch, med ændringer i de samme tabeller. I ender med:
- Konflikt i migrations-filerne
- Test-databasen der ikke matcher prod
- Nogen der manuelt retter i databasen for at “komme tilbage på sporet”
Pludselig er der tre versioner af sandheden: din lokale, CI-databasen og produktion.
Scenario 2: Destruktive ændringer direkte i produktion
En anden favorit: Nogen renamer en kolonne ved at droppe den gamle og oprette en ny i én migration. Backend-koden bliver deployet samtidig.
Hvad sker der så?
- Requests rammer databasen midt i ændringen
- Nogle queries forventer den gamle kolonne, andre den nye
- Prod logger pludselig fejl på hver anden request
Hvis I er hurtige, ruller I deployet tilbage. Hvis ikke, har I lige brudt noget for brugerne.
Scenario 3: Rollback der ikke ruller noget som helst tilbage
Sidste scenarie: Migrationen fejler midtvejs. I tænker “vi ruller bare tilbage”. Men:
- Nogle trin i migrationen har nået at køre
- Rollback-scriptet er enten generisk eller ikke testet
- Data er måske allerede slettet eller ændret
Så står I dér med en halvt opdateret database og et rollback-script, der mest af alt gør situationen endnu mere akavet.
Det er her, en bevidst strategi for roll-forward kontra rollback begynder at give mening.
Roll-forward som standard (og hvornår rollback faktisk giver mening)
Jeg plejer at tænke på databaseændringer som tidspunkter, ikke som ting man kan spole frem og tilbage i som en film.
Derfor giver en “roll-forward” tankegang som standard ofte mere ro i maven end “vi ruller bare tilbage”.
Hvad betyder roll-forward i praksis?
Roll-forward betyder: når noget går galt, retter du med en ny migration, i stedet for at prøve at køre en magisk omvendt version af den gamle.
Eksempel:
- Migration 12 tilføjer en kolonne
nicknametilusers - Den viser sig at være forkert (forkert type, ikke-null, hvad som helst)
- I stedet for at rulle 12 tilbage, laver du migration 13 der retter problemet
På den måde bevarer du en lineær historik: 1, 2, 3, …, 12, 13. Databasen kender kun én vej: frem.
Hvornår giver rollback mening?
Rollback kan give mening i to situationer:
- Lokalt eller i testmiljøer
Her bruger du rollback til hurtigt at nulstille databasen efter eksperimenter. - Helt tidligt i et incident
Hvis migrationen fejler meget tidligt, før den når at lave halve ændringer, og dit værktøj kan garantere transaktionel sikkerhed (alt eller intet).
Men den plan du læner dig op ad kl. 23:41, når prod er rød, bør være “lav en ny migrations-fix” frem for “rul 17 skridt tilbage i en ukendt database-tilstand”.
Workflow: fra feature-branch til produktion
Lad os bygge en arbejdsgang, der virker både til små teams og lidt større setups. Jeg tager udgangspunkt i noget a la Prisma Migrate, Flyway eller Liquibase, men mønstret er generelt.
1. Én sandhed: migrations er den officielle datamodel
Første regel: Ingen ændrer schema direkte i databasen. Ingen “jeg lige hurtigt retter i prod”.
Alt schema styres via migrations, og migrations ligger i versionsstyring (Git). Det er her, den officielle sandhed bor.
2. Ny feature, ny branch, ny migration
Når du laver en ny feature, der kræver schema-ændringer:
- Opret en feature-branch
- Lav schema-ændringerne i din ORM / SQL / migrationsværktøj
- Generér en ny migration og commit den i branchen
- Kør migrationen mod din lokale database
Test lokalt. Både kode og database skal være i sync dér, før du overhovedet tænker på at åbne en pull request.
3. Navngivning og rækkefølge så man ikke får grå hår
Navngiv migrations så det er til at gætte, hvad de gør. Ikke bare 20240403_132501.sql.
Eksempel på navngivning:
2024-04-03T13-25-add-nickname-to-users.sql2024-04-03T13-32-rename-fullname-to-name.sql
Brug timestamp + kort beskrivelse. De fleste værktøjer styrer rækkefølgen via enten nummer eller timestamp. Du skal bare sikre:
- Ingen manuelt modificerer gamle migrations
- Nye migrations ligger som nye filer i slutningen
Hvis to branches laver migrations parallelt, ender du typisk med merge-konflikt i versionsfilen (eller i mapperækkefølgen). Løsning:
- Merge main ind i din branch
- Generér en ny migration, der bygger videre på den nuværende state
- Slet den gamle, der ikke længere giver mening, inden du merger til main
Det føles irriterende den første uge, men det er markant mindre smertefuldt end at gætte på prod-state senere.
4. Pull request: migration og kode sammen
En vigtig regel: Migration og kode skal lande sammen.
Altså: den PR, der ændrer koden til at bruge en ny kolonne, skal også indeholde migrationen der tilføjer den kolonne.
Hvis I bruger noget ala CI pipelines, skal den:
- Spinne en frisk test-database op
- Køre alle eksisterende migrations i rækkefølge
- Køre tests bagefter
Fejler migrations i CI, kommer den aldrig tæt på produktion, og du har reddet dig selv for en sen aftenvagt.
5. Review: hvad skal man kigge efter i en migration?
Review af migrations er lidt en overset disciplin. De fleste scroller bare forbi og tænker “ser fint ud”.
Men der er nogle helt konkrete ting, jeg altid kigger efter.
Migration review-tjekliste
- Destruktive ændringer?
Drop af kolonner, tabeller eller constraints. Hvis ja: er der en plan for data, og skal det gøres i flere trin? - Lange blokeringer?
StoreALTER TABLEuden fornuftig strategi kan låse tabellen i lang tid. Især på store tabeller. - Data-migrering inline?
Hvis der stårUPDATE users SET ...på en kæmpe tabel, kan det være bedre som et batch-job i appen. - Default values og nullability?
Er nye kolonner medNOT NULLdækket af default eller data backfill? - Expand/contract?
Ved rename/ændring af type: er det splittet op i flere migrations, så man undgår downtime?
Hvis I arbejder med Prisma, kan det også give mening at kigge på schema-diff. Se hvordan deres Prisma Migrate beskriver de foreslåede ændringer, og om det matcher det, I forventer.
6. Deploy: migrations først, eller kode først?
Her er en simpel regel jeg selv bruger:
- Hvis det er en udvidelse (ny kolonne, der ikke bruges endnu): kør migrationen først, deploy kode bagefter.
- Hvis det er en kontraktion (fjerne noget kode, der holder op med at bruge en kolonne): deploy kode der ikke bruger feltet, kør migrations efter.
Hvis du vil gøre det ekstra sikkert, kan din deploy-pipeline se sådan her ud:
- Stop kortvarigt ny deploy-trafik (blue/green eller rolling, afhængigt af setup)
- Kør migrations mod databasen
- Rul ny kode ud
Det hænger også tæt sammen med næste mønster: zero downtime med expand → migrate → contract.
Zero downtime-mønster: expand → migrate → contract
Expand/contract er et lille mønster, som redder dig hver gang du skal lave større ændringer i databasen, uden at smadre produktion.
Case: rename af en kolonne uden at slå prod ihjel
Forestil dig, at du vil renam’e fullname til name i tabellen users.
Den naive løsning:
- Opret
name - Kopier data fra
fullnametilname - Slet
fullname - Opdater kode til at bruge
name
Hvis alt det ligger i én migration og sammen med én kode-deploy, har du et kort vindue hvor koden måske forventer name mens tabellen kun har fullname, eller omvendt.
Expand/contract deler det op i tre faser.
Fase 1: Expand
Du gør databasen mere tolerant, ikke mindre.
-- Migration A: expand
ALTER TABLE users ADD COLUMN name TEXT NULL;
Deploy kode der:
- Skriver til både
fullnameogname - Læser fra
namehvis ikke null, ellers falder tilbage tilfullname
Systemet virker stadig, gamle data lever, nye requests lander begge steder.
Fase 2: Migrate (data)
Nu skal eksisterende data over på den nye kolonne. Det kan ske på to måder:
- Som en database-migration med
UPDATE users SET name = fullname WHERE name IS NULL; - Som et batch-job i applikationen (f.eks. et cron-job eller engangs-script)
Pointen er: du har tid. Du kan endda overvåge hvor mange rækker der mangler, før du går videre.
Fase 3: Contract
Først når al data er flyttet, og du har været sikker et stykke tid, laver du “opstramningen”:
-- Migration B: contract
ALTER TABLE users DROP COLUMN fullname;
ALTER TABLE users ALTER COLUMN name SET NOT NULL;
Nu kan du også forenkle koden til kun at bruge name.
Samme mønster virker til:
- Ændring af datatyper
- Splitting af én tabel til to
- Indførsel af nye constraints
Det føles måske langsomt, men det er sådan du får zero downtime migrations i praksis.
CI der fanger farlige migrations automatisk
Hvis du har et CI-setup i forvejen, kan du gøre rigtig meget for at fange migrations-fejl tidligt. Især i små projekter bliver det her tit sprunget over, selvom det faktisk er nemt at få noget basalt på plads.
1. Kør migrations mod en frisk database i hver build
Det vigtigste: CI skal kunne spinne en helt frisk database op og køre alle migrations fra 0.
I pseudo-YAML kunne det ligne noget i stil med:
steps:
- name: Start database
run: docker run -d -p 5432:5432 postgres:16
- name: Kør migrations
run: npm run migrate:up
- name: Kør tests
run: npm test
Hvis en migration fejler, eller to migrations ikke kan leve i samme historik, opdager du det her, ikke i prod.
2. En simpel “farlig-migration” check
Du kan også lave små automatiske “lint checks” på dine migrations-filer.
Idé: skriv et lille script, der scanner migrations for bestemte mønstre:
DROP TABLEDROP COLUMNALTER TABLE ... SET NOT NULLuden default
Hvis scriptet finder dem, kan CI:
- Fejle buildet helt, eller
- Bare skrive en stor advarsel og kræve manuel godkendelse fra en ekstra reviewer
Det føles lidt hardcore første gang man rammer det, men det er meget billigere end at miste data ved et uheld.
3. Migrations i multi-miljø: dev, staging, prod
Din pipeline for migrations bør gå nogenlunde sådan her:
- Kør migrations i dev-miljøet (automatisk ved deploy)
- Kør de samme migrations i staging
- Når de har kørt fint dér, er de “godkendt” til prod
Her kan Flyway, Liquibase og lignende hjælpe med at logge præcis, hvilke migrations der er kørt hvor. Hvis du er helt ny til de tools, kan du kigge på Flyway-dokumentationen for at se, hvordan de registrerer versions-staten direkte i databasen.
Seed-data og testdatabaser uden at blande miljøer
En anden kilde til kaos er seed-scripts, der bliver kørt lidt vilkårligt i forskellige miljøer.
Hold schema og seed-data adskilt
Schema-migrations og seed-data er to forskellige ting:
- Migrations beskriver strukturen: tabeller, kolonner, constraints
- Seed beskriver indholdet du skal bruge: testbrugere, demo-data osv.
Jeg plejer at have to kommandoer:
npm run migrate:up # opdaterer schema
npm run db:seed # lægger testdata ind
Og så aldrig køre db:seed i produktion. Punktum.
Testdatabaser skal være billige at smide ud
Din testdatabase skal være noget, du ikke er bange for at droppe. I CI bliver den typisk oprettet og slettet automatisk.
Lokalt kan du gøre det nemt for dig selv:
npm run db:reset
# intern kommando der gør noget i stil med:
# - dropper database
# - opretter den igen
# - kører alle migrations
# - kører db:seed (hvis det er ønsket)
Jo nemmere det er at resette, jo mindre fristende er det at begynde at “håndlappe” fejl direkte i databasen.
Incident-plan: når migrationen fejler midt i et deploy
Nu til det ubehagelige: hvad gør du, hvis en migration fejler halvvejs i et deploy mod produktion?
Hvis du ikke har tænkt over det før, ender du ofte i en blanding af panik og manuelle rettelser, som du så skal leve med i årevis.
1. Stop deployet, frys traffik-ændringer
Første skridt: Stop med at skubbe mere kode ud. Hvis du bruger et deploy-værktøj, så lad det mislykkede deploy forblive i rød status, indtil du forstår problemet.
Hvis du kører blue/green eller rolling deploys, så sørg for at ny trafik ikke bliver skubbet over på en halv-migreret database uden at du ved hvorfor det fejler.
2. Find den præcise fejl i migrations-loggen
De fleste migrationsværktøjer har en log eller en migrations-tabel i databasen, som viser hvilken version der er sidst kørt, og hvor den fejlede.
Tjek:
- Hvilken migration er sidst kørt helt igennem?
- Hvilken statement fejlede i den seneste migration?
- Har noget af den problematiske statement nået at ændre data?
Hvis værktøjet bruger transaktioner omkring hele migrationen, kan du være heldig at alt er rullet tilbage automatisk. Hvis ikke, er det her, hvor roll-forward-strategien kommer i spil.
3. Vurder: kan vi køre en lille “hotfix-migration”?
I mange tilfælde kan du løse problemet ved at:
- Lave en ny migration, der retter tilstanden, så den matcher det kode-release du vil have liggende i prod
- Køre den migration manuelt mod prod (med meget stor respekt og chat-kanalen åben)
Eksempler:
- En kolonne blev tilføjet med forkert datatype → ny migration der tilføjer korrekt kolonne og migrerer data
- En constraint gjorde alt for mange rækker invalide → midlertidig fjernelse af constrainten, efterfulgt af data-oprydning
Det føles lidt ubehageligt at patche på prod, men forskellen er: her gør du det via værktøjet, så det er registreret i historikken, i stedet for at skrive tilfældige SQL-kommandoer direkte i klienten.
4. Rollback af kode, ikke nødvendigvis database
Hvis du skal rulle noget tilbage i et incident, så lad det hellere være kode-deployet end databasen.
Koden er ofte nemmere at flytte frem og tilbage (nye builds, ny container-version, etc.). Databasen er mere “sticky”, fordi den rummer dine brugeres data.
Så hvis du står mellem:
- A: køre et rollback-script på databasen og håbe det rammer rigtigt
- B: rulle kode-deployet tilbage til en version, der stadig kan tale med den nuværende database-state
… så ville jeg næsten altid vælge B, og så bagefter rette databasen roligt med en ny migration.
5. Skriv incidentet ned (og opdater jeres workflow)
Hvis I en dag har et migrations-incident, og det kostede jer en aften, så brug lige 20 minutter bagefter på at skrive ned, hvad der skete.
Og vigtigere: opdater jeres workflow. Måske skal I:
- Lægge en ekstra CI-check på destruktive migrations
- Indføre expand/contract som standard, når noget renames
- Have en regel om, at migrations ikke merges sent fredag eftermiddag
Det er kedeligt process-arbejde, men det er også forskellen på at turde lave databaseændringer og at udskyde dem i måneder, fordi ingen gider være den, der trykker på knappen.







Send kommentar
Du skal være logget ind for at skrive en kommentar.