Skip to content

Migrations in Team Development

Come funziona Pragmatic.Migrations quando più sviluppatori lavorano in parallelo su branch diverse e poi fanno merge. Leggi prima il README di Pragmatic.Migrations.

Pragmatic.Migrations non usa file di migration numerati come EF Core. Funziona per diff dichiarativo:

  1. Compile-time — il source generator scansiona le entity [Entity] e produce uno schema desiderato (SchemaVersion), identificato da un hash.
  2. Runtime — all’avvio il runner introspeziona lo schema reale del database, calcola il diff verso lo schema desiderato e lo applica.

Non esistono file di migration: lo “stato target” è sempre e solo ciò che le entity del codice descrivono adesso.

Scenario tipico:

  • Branch A aggiunge l’entity Invoice.
  • Branch B aggiunge l’entity Payment.
  • Entrambe partono da main e vengono mergiate.

A differenza di EF Core — dove due file di migration creano un conflitto git esplicito sul ModelSnapshot — qui il merge git riguarda solo le classi entity. Se il merge è corretto (entrambe le classi Invoice e Payment finiscono su main), lo schema desiderato le contiene entrambe e tutto funziona: il diff produce due CreateTable, non-breaking, applicati senza intervento.

Il rischio si presenta quando il merge è risolto male: se durante la risoluzione dei conflitti una delle due classi entity viene persa (o un file non viene aggiunto), lo schema desiderato risultante non conterrà quell’entity. Git non segnala nulla — non c’è un file di migration su cui collidere.

La rete di sicurezza: breaking-change gate

Section titled “La rete di sicurezza: breaking-change gate”

Questo non si traduce in una perdita dati silenziosa. Quando il runner calcola il diff tra lo schema desiderato (senza l’entity persa) e il database reale (che ha già la tabella), produce un DropTable.

DropTable e DropColumn sono marcati IsBreaking: true. Il runner (MigrationRunner) blocca l’esecuzione se il diff contiene breaking change e MigrationOptions.Force è false:

[App] Blocked: 1 breaking changes. Use Force=true.
1 breaking change(s) detected — these may cause data loss
Run with DryRun=true to inspect the SQL before applying
Use Force=true to apply breaking changes

Force ha default false e app.UsePragmaticMigrations() non lo abilita. Quindi un merge che fa sparire un’entity ferma l’avvio dell’applicazione invece di cancellare la tabella.

ChangeBreaking?Note
CreateTableNo
AddColumnNo
RenameColumnNoRilevata solo con [RenamedFrom] sulla property
AlterColumnDefaultNo
DropTable
DropColumn
AlterColumnTypeSì se narrowingEs. varchar(256)varchar(100)
AlterColumnNullabilitySì se diventa NOT NULLDati esistenti NULL lo violerebbero
  1. DryRun in CI — esegui la migration con MigrationOptions { DryRun = true } in pipeline su un DB allineato a produzione. Stampa la lista delle change; se compaiono DropTable/DropColumn inattesi, il merge ha perso qualcosa.
  2. Rebase frequente su main — riduce la finestra in cui due branch divergono sullo schema.
  3. Una entity per PR, quando possibile — minimizza i conflitti che toccano lo schema.
  4. Tratta il blocco breaking-change come un segnale, non un ostacolo — se all’avvio vedi DROP TABLE X e nessuno ha intenzionalmente rimosso l’entity X, è quasi certamente un merge incompleto: non mettere Force=true, ripristina l’entity mancante.
  5. Force=true solo per rimozioni intenzionali — e dopo aver verificato il SQL con DryRun.
  6. Backup prima di ogni Force — vale come per qualsiasi DDL distruttivo.
  • Non c’è detection a compile-time del tipo “il codice non dichiara più un’entity che esisteva su main”: il source generator vede solo lo stato corrente del codice. La rete di sicurezza è interamente a runtime (breaking-change gate).
  • Lo schema desiderato dipende dal codice mergiato, non dall’ordine di merge: due merge che producono lo stesso insieme di entity producono lo stesso schema.
  • Schema tenant-specifici (colonne custom per singolo tenant) non sono supportati: lo schema desiderato è globale. Per variazioni per-tenant usare flag/colonne opzionali a livello globale.

Il merge concorrente è sicuro finché il merge git delle classi entity è corretto. Se non lo è, il danno non è silenzioso: il breaking-change gate ferma l’applicazione e mostra esattamente quali tabelle/colonne verrebbero eliminate. La regola operativa è una sola: un DROP inatteso all’avvio = merge da rivedere, non Force da aggiungere.