Alembic: DB Schema Migrations in Python

Alembic è lo strumento di riferimento per gestire in modo smart versionamento e migrazioni di database in Python. Alembic è una soluzione standalone indipendente dal framework, un tool perfetto per progetti di qualsiasi natura (che usano SQLAlchemy).

Alembic non è perfetto in qualsiasi contesto. Per esempio Django ha già un sistema di migrazioni integrato, strettamente accoppiato alla sua ORM e ai suoi comandi (makemigrations per generare i file e migrate per applicarli). Alembic ha senso quando lo strato dati è SQLAlchemy-centric (FastAPI/Flask/CLI/ETL con SQLAlchemy, SQLModel, ecc.), mentre perde senso quando l’ORM e il framework portano già un sistema di migrazioni ufficiale e usato dall’ecosistema

Cosa fa Alembic

In sostanza con Alembic posso tracciare facilmente le modifiche dello schema DB nel tempo.
Con Alembic posso:

  • Generare migrazioni automaticamente dal codice dei modelli
  • Avanzare e fare rollback delle modifiche al database in modo controllato
  • Tracciare la storia di tutte le trasformazioni dello schema
  • Gestire branch di migrazioni complesse in progetti multi-team

Permette quindi di versionare il database esattamente come versioniamo il codice con Git.

Installazione e Inizializzazione

In questa guida uso uv come gestore di pacchetti e come runner dei comandi (ad esempio uv run …), perché rende più semplice mantenere ambiente e dipendenze allineati al progetto. I passaggi sono concettualmente identici con qualunque altro package manager in Python: basta sostituire i comandi di installazione/sync e il prefisso di esecuzione (es. uv run) con l’equivalente nel tuo stack (virtualenv + pip, Poetry, PDM, ecc.), lasciando invariati i comandi di Alembic e la logica del workflow.

Il primo passo è installare Alembic nel progetto. Con uv, il moderno package manager Python, il comando è:

uv add alembic

Questo aggiungerà Alembic alle dipendenze del progetto (nel file pyproject.toml).

Quindi, si comincia inizializzando Alembic:

uv run alembic init migrations

Questo crea una cartella migrations/ con la seguente struttura standard:

migrations/
├── alembic.ini          # Configurazione principale
├── env.py               # Ambiente di esecuzione
├── script.py.mako       # Template per le migrazioni
└── versions/            # Cartella con i file di migrazione

Configurazione di Alembic con PostgreSQL

Nel file alembic.ini, occorre configurare la connessione al database. La variabile più importante è sqlalchemy.url. Nel workflow si può configurare attraverso l’uso di variabili d’ambiente:

sqlalchemy.url = driver://user:password@localhost/dbname

È anche possibile configurare il tutto attraverso il file env.py:

import os
from sqlalchemy import engine_from_config
from sqlalchemy import pool

# Leggi da variabili d'ambiente
db_host = os.getenv("POSTGRES_HOST", "localhost")
db_port = os.getenv("POSTGRES_PORT", "5432")
db_name = os.getenv("POSTGRES_DB", "mydb")
db_user = os.getenv("POSTGRES_USER", "postgres")
db_password = os.getenv("POSTGRES_PASSWORD", "")

config.set_main_option(
    "sqlalchemy.url",
    f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
)

Comandi Principali

Ecco i comandi Alembic più comuni.

1. Controllare lo stato attuale

uv run alembic current

Mostra quale migrazione è attualmente applicata al database. Utile per capire dove ci si trova nel flusso di migrazioni.

uv run alembic heads

Mostra il l’head della sequenza di migrazioni (e gli altri head se si gestiscono branch).
Di solito si dovrebbe avere un solo head (il punto più recente).

2. Verificare le modifiche non migrate

uv run alembic check

Confronta il modello SQLModel/SQLAlchemy con lo schema del database.
Se le colonne o le tabelle nel codice non corrispondono al database, Alembic lo segnala.

Questo è fondamentale prima di generare una nuova migrazione.

Se non si sono impostate le variabili di ambiente è possibile lanciare i comandi includendole all’inizio: Per esempio:

POSTGRES_HOST=localhost POSTGRES_PORT=5433 POSTGRES_DB=miodb uv run alembic check

3. Generare una migrazione automatica

uv run alembic revision --autogenerate -m "Descrizione della modifica"

Questo è il comando che si finisce per utilizzare più frequentemente. È il corrispettivo del commit Git. Quando lanciato Alembic analizza i modelli e il database, e genera automaticamente un file di migrazione.
Ad esempio:

POSTGRES_HOST=localhost POSTGRES_PORT=5433 POSTGRES_DB=miodb \
  uv run alembic revision --autogenerate -m "Add vel field on table measures"

La migrazione viene salvata in migrations/versions/ con un nome come abcd1234ef56_add_vel_field_on_table_measures.py.

Un file di migrazione Alembic (per intenderci, quello appena generato in migrations/versions/…py) è uno script Python “versionato” che descrive cosa cambia nello schema e come tornare indietro. In genere contiene:

  • Metadati di revisione: revision (ID univoco della migration), down_revision (ID della migration precedente), più eventuali branch_labels e depends_on, che permettono ad Alembic di costruire il grafo delle dipendenze tra migrazioni.
  • Import e contesto: import di alembic.op (l’helper per emettere operazioni DDL) e di sqlalchemy as sa per tipi/colonne/vincoli; c’è anche un docstring con descrizione e timestamp.
  • Due funzioni chiave:
    • upgrade(): applica la modifica (es. op.add_column, op.drop_column, op.create_table, op.alter_column, ecc.).
    • downgrade(): definisce l’operazione inversa per fare rollback (idealmente il mirror dell’upgrade).
  • Operazioni generate: nel caso di --autogenerate ci si troverà di mezzo anche blocchi commentati tipo # ### commands auto generated by Alembic ### che indicano le istruzioni create automaticamente, da rivedere/raffinare prima di applicarle.

4. Applicare le migrazioni

uv run alembic upgrade head

Applica tutte le migrazioni in pending fino all’ultima. Questo aggiorna il database alla versione più recente del codice.

Se si vuole applicare solo una specifica migrazione:

uv run alembic upgrade <revision_id>

5. Rollback di una migrazione

OK, supponiamo di aver cambiato idea e di voler tornare indietro di n passaggi:

uv run alembic downgrade -1

Torna indietro di una migrazione.

Volendo si può anche indicare un id specifico:

uv run alembic downgrade <revision_id>

Questo è un aspetto notevole da usare in produzione perché permette di fare rollback senza perdere dati (se la migrazione è stata scritta correttamente prevedendo le logiche di gestione dei dati nella stessa funzione di rollback).

Gestire Migrazioni

Nella mia piccola esperienza mi sono trovato a gestire e affrontare migrazioni non troppo complesse: rinominare colonne, aggiungerlo o rimuoverle, gestire chiavi esterne, etc.
Ecco alcune situazioni che ho affrontato e incontrato che credo siano da ricordare:

  • Rinominare campi con preservazione dati: Quando Alembic genera una migrazione per rinominare una colonna, di solito lo fa correttamente con ALTER TABLE ... RENAME COLUMN. Però, prima di applicare la migrazione, è bene verificare che i dati siano compatibili.
  • Se si aggiunge una colonna NOT NULL su una tabella popolata, ci sono due strade:
    1) Aggiungere la colonna nullable, popolare i valori per le righe esistenti, poi renderla NOT NULL.
    2) Aggiungere la colonna NOT NULL con un default, così il DB può assegnare un valore alle righe esistenti; poi, se non si vuole mantenere il default assegnato a livello DB per le nuove insert, si può rimuoverlo con un alter_column(..., server_default=None).

Esempio opzione 2, con rimozione del default (approccio preferibile quando si vuole evitare un update massivo manuale e basta un valore default per le righe già esistenti.):

op.add_column(
    "my_table",
    sa.Column("new_column", sa.String(), nullable=False, server_default="default_value"),
)
op.alter_column("my_table", "new_column", server_default=None)
  • Gestire chiavi esterne: Quando si aggiunge o modifica foreign key, assicurarsi che i dati nel database rispettino il vincolo. Alembic potrebbe fallire se i dati sono inconsistenti.
  • Errore: “Target database is not up to date”. Dopo l’errore provare a lanciare:
uv run alembic current
uv run alembic heads

Banalmente se il current è diverso da heads, significa che il database è dietro rispetto alle migrazioni disponibili. Occorre applicare le migrazioni pending:

uv run alembic upgrade head
  • Errore: “Multiple heads detected”

Questo accade quando due branch di sviluppo hanno creato migrazioni divergenti. Occorre risolvere il conflitto manualmente facendo merge delle migrazioni o creando una nuova migrazione che dipende da entrambi gli head.

  • Perché si verifica:
    Due o più sviluppatori hanno creato migrazioni partendo dallo stesso punto, oppure si è passato tra rami git senza sincronizzare le migrazioni.
    Questo genera più head nel repository delle migrazioni e Alembic non può generare una nuova revisione finché non si risolve la divergenza.
  • Come risolvere:
    Identificare gli heads divergenti: eseguire comandi come alembic heads o alembic history per vedere gli heads attuali.
  • Risolvere manualmente la divergenza:
    • Opzione 1: merge gli heads esistenti, creando una migrazione di merge che dipenda da entrambe le teste (merge head) e poi continuare a generare nuove revisioni.
    • Opzione 2: ricollegare le revisioni duplicando o ri-lavorando i file di migrazione per creare una sequenza lineare, aggiornando i campi down_revision di una o più migrazioni per puntare alla testa corretta (soluzione comune, ma va fatta con cautela per le modifiche effettive al DB).
  • Dopo il merge o la correzione, eseguire alembic upgrade head per applicare le migrazioni corrette al database.
  • Buone pratiche per prevenire:
    • Pull prima di creare nuove migrazioni, per allineare i rami e ridurre divergenze.
    • Coordinare le modifiche allo schema tra sviluppatori, soprattutto su tabelle comuni.
    • Evitare branch lunghi e confusi: integrare regolarmente main into feature branches e completare una migrazione prima di iniziare la successiva.
  • Errore: “No changes detected”

Se si genera una migrazione… ma poi Alembic dice che non ci sono cambiamenti.
Controllare che:

  • I modelli SQLModel siano importati correttamente in env.py
  • Che target_metadata punti ai modelli del progetto
  • Che il database sia realmente diverso dai modelli

Immagine copertina: Iconographie descriptive des cactées, ou, Essais systématiques et raisonnés sur l’histoire naturelle, la classification et la culture des plantes de cette famille

Summary
Article Name
Alembic per migrazioni e versionamento DB in Python
Description
Usare Alembic per versioning DB in Python: setup, configurazione PostgreSQL, comandi (revision, upgrade, downgrade) e altro.
Author
Pubblicato in Python.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *