Prisma migrations: come gestire l'evoluzione del database in produzione

Da migrate dev a migrate deploy, passando per shadow database, rollback e migration che non perdono dati: la parte di Prisma che i tutorial saltano sempre.

Prisma Migrations

Il setup di Prisma è la parte facile. Lo abbiamo raccontato in Prisma con Next.js: setup, schema e query in un progetto reale. Installazione, schema, prime query: tutto funziona in un pomeriggio. Il problema arriva dopo, quando l'applicazione è in produzione con dati veri e lo schema deve cambiare. Un campo da rinominare, una tabella da spezzare in due, una colonna che da opzionale diventa obbligatoria. È qui che si vede la differenza tra chi ha seguito un tutorial e chi ha gestito un database che non può permettersi di perdere una riga.

Prisma ha un sistema di migration solido, ma dà per scontato che tu sappia come usarlo. Questo articolo copre esattamente quel pezzo: cosa succede quando il database evolve nel tempo, e come farlo evolvere senza incidenti.

Qual è la differenza tra prisma migrate dev e prisma migrate deploy

La differenza è netta: migrate dev genera e applica migration in sviluppo, e può resettare il database se rileva incongruenze; migrate deploy applica solo le migration pendenti, senza generare né resettare nulla, ed è l'unico dei due da usare in produzione. Eppure il primo errore, ed è sorprendentemente comune, è proprio lanciare prisma migrate dev su un database di produzione. I due comandi hanno nomi simili e scopi opposti.

prisma migrate dev è uno strumento di sviluppo. Confronta lo schema Prisma con lo stato attuale del database, genera una nuova migration SQL, la applica, e rigenera il client. Ma fa anche altro: se rileva una deriva tra la migration history e il database reale (il cosiddetto drift), può proporre di resettare il database, cancellando tutti i dati. In locale è un comportamento utile. Su un database di produzione è una catastrofe.

prisma migrate deploy è il comando pensato per produzione e per le pipeline CI/CD. Non genera niente, non chiede niente, non resetta niente. Fa una cosa sola: applica in ordine le migration presenti nella cartella prisma/migrations che non risultano ancora applicate al database. Se qualcosa non torna, fallisce con un errore invece di improvvisare.

# In locale, mentre sviluppi
npx prisma migrate dev --name aggiungi-campo-telefono

# In produzione, nella pipeline di deploy
npx prisma migrate deploy

La regola pratica: migrate dev non deve mai comparire in uno script di deploy, in un Dockerfile di produzione o in una pipeline. Se compare, prima o poi presenterà il conto.

La migration history è un registro, non una bozza

Una migration già applicata non si modifica mai a mano: Prisma verifica un checksum per ogni file, e qualsiasi ritocco rompe l'integrità della history su tutti gli ambienti che l'hanno già eseguita. Per capire perché, serve vedere come funziona il registro. Ogni volta che lanci migrate dev, Prisma crea una cartella dentro prisma/migrations con un timestamp e il nome che hai scelto. Dentro c'è il file migration.sql con le istruzioni DDL generate. In parallelo, nel database, Prisma mantiene una tabella chiamata _prisma_migrations che registra quali migration sono state applicate, quando, e con quale checksum.

Quel checksum è il punto delicato. Prisma calcola un hash del contenuto di ogni file di migration e lo confronta con quello registrato nel database. Se modifichi a mano un file di migration già applicata, anche solo per sistemare un commento, i checksum non coincidono più e Prisma segnala un errore di integrità. Da quel momento la history è inconsistente su ogni ambiente che aveva già applicato la versione precedente del file.

Le migration già applicate vanno trattate come commit già pushati su un branch condiviso: si va avanti, non si riscrive il passato. Serve una correzione? Si crea una nuova migration che corregge. L'unica eccezione ragionevole è una migration appena creata in locale e non ancora applicata da nessun altro ambiente: quella si può ancora ritoccare, o cancellare e rigenerare.

Come cambiare lo schema senza perdere dati

La risposta breve: si genera la migration con --create-only, si corregge l'SQL a mano e solo dopo la si applica. Prisma infatti genera l'SQL guardando la differenza tra due schemi, ma non conosce le tue intenzioni. Un esempio classico: rinominare il campo name in fullName. Per te è una rinomina. Per il diff automatico sono due operazioni distinte: eliminare la colonna name e crearne una nuova fullName, vuota. Risultato: tutti i valori esistenti persi.

Lo stesso problema si presenta in altri scenari ricorrenti:

  • Campo da opzionale a obbligatorio: se esistono righe con valore NULL, l'ALTER TABLE fallisce o, peggio, richiede un default che sovrascrive il significato del dato
  • Cambio di tipo: da String a Int, il cast automatico può fallire su valori non convertibili
  • Spostamento di dati tra tabelle: il diff crea la struttura nuova ma non sposta niente

La soluzione non è evitare le migration, è controllarle. E per farlo serve il flag giusto.

--create-only: leggere l'SQL prima di applicarlo

Il flag --create-only cambia il comportamento di migrate dev: genera il file migration.sql ma non lo applica. Ti lascia una finestra per aprirlo, leggerlo, e modificarlo prima che tocchi il database.

npx prisma migrate dev --create-only --name rinomina-name-in-fullname

Per la rinomina di cui sopra, l'SQL generato sarà qualcosa come:

ALTER TABLE "User" DROP COLUMN "name";
ALTER TABLE "User" ADD COLUMN "fullName" TEXT NOT NULL;

Distruttivo. Lo si sostituisce a mano con la vera intenzione:

ALTER TABLE "User" RENAME COLUMN "name" TO "fullName";

Poi si rilancia npx prisma migrate dev senza flag, e Prisma applica il file modificato. Lo stesso approccio funziona per i casi più articolati. Un campo che diventa obbligatorio si gestisce in due passaggi dentro la stessa migration: prima si valorizzano i NULL esistenti con un UPDATE, poi si aggiunge il vincolo NOT NULL.

UPDATE "User" SET "phone" = 'non disponibile' WHERE "phone" IS NULL;
ALTER TABLE "User" ALTER COLUMN "phone" SET NOT NULL;

--create-only è probabilmente il singolo comando che separa un uso amatoriale di Prisma da un uso professionale. Ogni migration che tocca dati esistenti dovrebbe passare da qui.

Prisma supporta il rollback delle migration?

No. In Prisma non esiste un comando migrate down: le migration sono pensate per andare solo in avanti (forward-only). Chi arriva da strumenti come Rails o da librerie come Knex se lo aspetta, ma la scelta è dichiarata nella documentazione ufficiale, e ha una logica: un rollback automatico su un database con dati reali è spesso un'illusione. Se una migration ha eliminato una colonna, nessun comando può far riapparire i dati che conteneva.

Quello che Prisma offre per le emergenze è prisma migrate resolve, che non modifica il database ma corregge la contabilità nella tabella _prisma_migrations:

# Segna una migration fallita come annullata, dopo che hai
# ripristinato manualmente il database
npx prisma migrate resolve --rolled-back 20260706_migration_fallita

# Oppure la segna come applicata, se l'hai completata a mano
npx prisma migrate resolve --applied 20260706_migration_fallita

Lo scenario tipico: migrate deploy fallisce a metà in produzione, magari per un vincolo violato. La migration risulta in stato failed e blocca ogni deploy successivo. La procedura corretta è capire cosa è stato applicato e cosa no, riportare il database a uno stato coerente (a mano, o da backup), e poi usare resolve per allineare la history.

Da qui una conseguenza operativa che vale più di qualsiasi comando: il backup prima di ogni migration in produzione non è opzionale. Su PostgreSQL bastano un pg_dump nella pipeline o gli snapshot del provider. Per i cambi di schema ad alto rischio, la strategia più robusta resta il pattern expand and contract: prima si aggiunge la struttura nuova accanto alla vecchia, si migra il codice, e solo in una migration successiva si rimuove la parte obsoleta. Ogni passaggio è reversibile perché niente viene distrutto finché tutto non è verificato.

Cos'è lo shadow database e perché Prisma lo richiede

Lo shadow database è un database temporaneo che Prisma crea e distrugge automaticamente a ogni migrate dev, riapplicandoci da zero l'intera migration history. Serve a due cose: verificare che le migration esistenti siano ancora valide e riproducibili, e rilevare il drift confrontando il risultato con il database di sviluppo reale. È il pezzo che genera più confusione al primo incontro, perché lavora dietro le quinte finché tutto va bene.

Per questo migrate dev richiede i permessi di creazione database. In locale, con un PostgreSQL installato via Docker, non te ne accorgi nemmeno. Con database cloud gestiti dove l'utente non può creare database, il comando fallisce. In quel caso si crea manualmente un database di appoggio e lo si dichiara nello schema:

datasource db {
  provider          = "postgresql"
  url               = env("DATABASE_URL")
  shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}

Nota importante: lo shadow database riguarda solo lo sviluppo. migrate deploy non lo usa e non ne ha bisogno, quindi in produzione questa configurazione non serve a niente. Un motivo in più per tenere i due comandi concettualmente separati.

Il database cambia comunque

Lo schema perfetto al primo colpo non esiste. Nei progetti che seguiamo, l'evoluzione del modello dati accompagna tutta la vita dell'applicazione, e la differenza la fa il processo: migration versionate nel repository, SQL riletto prima di toccare dati esistenti, backup sistematici, e due comandi che non si confondono mai tra loro. Prisma dà gli strumenti per farlo bene. Il prossimo passo naturale della serie è il deploy vero e proprio: connection pooling, serverless e cosa succede a Prisma quando le connessioni al database diventano centinaia.

Il deploy in serverless, con centinaia di connessioni al database, è il capitolo dove le cose si complicano davvero. Se non volete aspettare il prossimo articolo, prenotate un appuntamento: la vostra architettura la analizziamo caso per caso.

Autoreadmin
Potrebbero interessarti...
back to top icon