1. Panoramica del meccanismo di migrazione

1.1 Concetto e ruolo della migrazione

La migrazione del database è il processo di sincronizzazione delle modifiche nei modelli di dati con la struttura del database, che è essenziale per la persistenza dei dati. Con l'iterazione della versione dell'applicazione, il modello di dati subisce spesso modifiche, come l'aggiunta o la cancellazione di campi, o la modifica degli indici. La migrazione consente ai developer di gestire queste modifiche in modo versionato e sistematico, garantendo la coerenza tra la struttura del database e il modello di dati.

Nell'ambito dello sviluppo web moderno, il meccanismo di migrazione fornisce i seguenti vantaggi:

  1. Controllo delle versioni: I file di migrazione possono tracciare la cronologia delle modifiche alla struttura del database, rendendo comodo tornare indietro e comprendere le modifiche in ciascuna versione.
  2. Deployment automatizzato: Attraverso il meccanismo di migrazione, il deployment e gli aggiornamenti del database possono essere automatizzati, riducendo la possibilità di interventi manuali e il rischio di errori.
  3. Collaborazione di squadra: I file di migrazione garantiscono che i membri del team utilizzino strutture di database sincronizzate in diversi ambienti di sviluppo, facilitando lo sviluppo collaborativo.

1.2 Caratteristiche di migrazione del framework ent

L'integrazione del framework ent con il meccanismo di migrazione offre le seguenti caratteristiche:

  1. Programmazione dichiarativa: I developer devono solo concentrarsi sulla rappresentazione in Go delle entità, e il framework ent si occuperà della conversione delle entità in tabelle del database.
  2. Migrazione automatica: ent può creare e aggiornare automaticamente le strutture delle tabelle del database senza la necessità di scrivere manualmente le istruzioni DDL.
  3. Controllo flessibile: ent fornisce diverse opzioni di configurazione per supportare diversi requisiti di migrazione, come con o senza vincoli di chiave esterna e generazione di ID globalmente univoci.

2. Introduzione alla migrazione automatica

2.1 Principi di base della migrazione automatica

La caratteristica di migrazione automatica del framework ent si basa sui file di definizione dello schema (tipicamente trovati nella directory ent/schema) per generare la struttura del database. Dopo che i developer definiscono entità e relazioni, ent ispezionerà la struttura esistente nel database e genererà operazioni corrispondenti per creare tabelle, aggiungere o modificare colonne, creare indici, ecc.

Inoltre, il principio di migrazione automatica di ent funziona in modalità "aggiunta": di default aggiunge solo nuove tabelle, nuovi indici o aggiunge colonne alle tabelle, e non elimina tabelle o colonne esistenti. Questo design è utile per evitare la perdita accidentale di dati e rende facile espandere la struttura del database in avanti.

2.2 Utilizzo della migrazione automatica

I passaggi di base per utilizzare la migrazione automatica di ent sono i seguenti:

package main

import (
    "context"
    "log"
    "ent"
)

func main() {
    client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
    if err != nil {
        log.Fatalf("Connessione a MySQL fallita: %v", err)
    }
    defer client.Close()
    ctx := context.Background()

    // Esegue la migrazione automatica per creare o aggiornare lo schema del database
    if err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("Creazione dello schema del database fallita: %v", err)
    }
}

Nel codice sopra, ent.Open è responsabile dell'instaurazione di una connessione con il database e del ritorno di un'istanza del client, mentre client.Schema.Create esegue effettivamente l'operazione di migrazione automatica.

3. Applicazioni avanzate della migrazione automatica

3.1 Eliminare colonne e indici

In alcuni casi, potremmo dover rimuovere colonne o indici non più necessari dallo schema del database. In questo caso, possiamo utilizzare le opzioni WithDropColumn e WithDropIndex. Ad esempio:

// Esegue la migrazione con opzioni per eliminare colonne e indici.
err = client.Schema.Create(
    ctx,
    migrate.WithDropIndex(true),
    migrate.WithDropColumn(true),
)

Questo snippet di codice abilita la configurazione per eliminare colonne e indici durante la migrazione automatica. ent eliminerà eventuali colonne e indici che non esistono nella definizione dello schema durante l'esecuzione della migrazione.

3.2 ID globalmente univoci

Per impostazione predefinita, le chiavi primarie nei database SQL partono da 1 per ciascuna tabella, e diversi tipi di entità possono condividere lo stesso ID. In alcuni scenari di applicazione, come ad esempio quando si utilizza GraphQL, potrebbe essere necessario fornire univocità globale per gli ID degli oggetti di diversi tipi di entità. In ent, questo può essere configurato utilizzando l'opzione WithGlobalUniqueID:

// Esegue la migrazione con ID univoci globali per ciascuna entità.
if err := client.Schema.Create(ctx, migrate.WithGlobalUniqueID(true)); err != nil {
    log.Fatalf("Creazione dello schema del database fallita: %v", err)
}

Dopo aver abilitato l'opzione WithGlobalUniqueID, ent assegnerà un ID di intervallo 2^32 a ciascuna entità in una tabella chiamata ent_types per ottenere univocità globale.

3.3 Modalità offline

La modalità offline consente di scrivere le modifiche dello schema su un io.Writer anziché eseguirle direttamente sul database. È utile per verificare i comandi SQL prima che le modifiche abbiano effetto oppure per generare uno script SQL per l'esecuzione manuale. Ad esempio:

// Scrivi le modifiche della migrazione su un file
f, err := os.Create("migra.sql")
if err != nil {
    log.Fatalf("Impossibile creare il file di migrazione: %v", err)
}
defer f.Close()
if err := client.Schema.WriteTo(ctx, f); err != nil {
    log.Fatalf("Impossibile stampare le modifiche dello schema del database: %v", err)
}

Questo codice scriverà le modifiche della migrazione su un file chiamato migra.sql. Nella pratica, gli sviluppatori possono scegliere di stampare direttamente in output standard o di scrivere su un file per la revisione o la registrazione.

4. Supporto per chiavi esterne e hook personalizzati

4.1 Abilita o Disabilita le Chiavi Esterne

In Ent, le chiavi esterne sono implementate definendo le relazioni (bordi) tra entità, e queste relazioni di chiave esterna vengono create automaticamente a livello di database per garantire l'integrità e la coerenza dei dati. Tuttavia, in determinate situazioni, ad esempio per l'ottimizzazione delle prestazioni o quando il database non supporta le chiavi esterne, è possibile scegliere di disabilitarle.

Per abilitare o disabilitare i vincoli delle chiavi esterne nelle migrazioni, è possibile controllarlo attraverso l'opzione di configurazione WithForeignKeys:

// Abilita le chiavi esterne
err = client.Schema.Create(
    ctx,
    migrate.WithForeignKeys(true), 
)
if err != nil {
    log.Fatalf("Impossibile creare risorse dello schema con chiavi esterne: %v", err)
}

// Disabilita le chiavi esterne
err = client.Schema.Create(
    ctx,
    migrate.WithForeignKeys(false), 
)
if err != nil {
    log.Fatalf("Impossibile creare risorse dello schema senza chiavi esterne: %v", err)
}

Questa opzione di configurazione deve essere passata durante la chiamata a Schema.Create, e determina se includere i vincoli delle chiavi esterne nel DDL generato in base al valore specificato.

4.2 Applicazione degli Hook di Migrazione

Gli hook di migrazione sono logiche personalizzate che possono essere inserite ed eseguite in fasi diverse dell'esecuzione della migrazione. Sono molto utili per eseguire logiche specifiche sul database prima/dopo la migrazione, come ad esempio la convalida dei risultati della migrazione e il pre-riempimento dei dati.

Ecco un esempio di come implementare gli hook di migrazione personalizzati:

func customHook(next schema.Creator) schema.Creator {
    return schema.CreateFunc(func(ctx context.Context, tables ...*schema.Table) error {
        // Codice personalizzato da eseguire prima della migrazione
        // Ad esempio, logging, verifica di determinate precondizioni, ecc.
        log.Println("Logica personalizzata prima della migrazione")
        
        // Chiama l'hook successivo o la logica di migrazione predefinita
        err := next.Create(ctx, tables...)
        if err != nil {
            return err
        }
        
        // Codice personalizzato da eseguire dopo la migrazione
        // Ad esempio, pulizia, migrazione dei dati, controlli di sicurezza, ecc.
        log.Println("Logica personalizzata dopo la migrazione")
        return nil
    })
}

// Utilizzo degli hook personalizzati nella migrazione
err := client.Schema.Create(
    ctx,
    schema.WithHooks(customHook),
)
if err != nil {
    log.Fatalf("Errore nell'applicare gli hook di migrazione personalizzati: %v", err)
}

Gli hook sono strumenti potenti e indispensabili per migrazioni complesse, offrendo la possibilità di controllare direttamente il comportamento della migrazione del database quando necessario.

5. Migrazioni versionate

5.1 Introduzione alle Migrazioni Versionate

La migrazione versionata è un modello per gestire la migrazione del database, consentendo agli sviluppatori di dividere le modifiche alla struttura del database in più versioni, ognuna contenente un insieme specifico di comandi di modifica del database. Rispetto alla Migrazione Automatica, la migrazione versionata fornisce un controllo più dettagliato, garantendo tracciabilità e reversibilità delle modifiche alla struttura del database.

Il principale vantaggio della migrazione versionata è il supporto alla migrazione in avanti e all'indietro (ovvero, l'aggiornamento o il downgrade), consentendo agli sviluppatori di applicare, annullare o saltare modifiche specifiche secondo necessità. Collaborando all'interno di un team, la migrazione versionata garantisce che ciascun membro lavori sulla stessa struttura del database, riducendo i problemi causati dalle incongruenze.

La migrazione automatica è spesso irreversibile, generando ed eseguendo istruzioni SQL per corrispondere allo stato più recente dei modelli di entità, utilizzata principalmente nelle fasi di sviluppo o in progetti di piccole dimensioni.

5.2 Utilizzo delle Migrazioni Versionate

1. Installazione dello Strumento Atlas

Prima di utilizzare le migrazioni versionate, è necessario installare lo strumento Atlas sul tuo sistema. Atlas è uno strumento di migrazione che supporta multiple sistemi di database, fornendo potenti funzionalità per gestire le modifiche dello schema del database.

macOS + Linux

curl -sSf https://atlasgo.sh | sh

Homebrew

brew install ariga/tap/atlas

Docker

docker pull arigaio/atlas
docker run --rm arigaio/atlas --help

Windows

https://release.ariga.io/atlas/atlas-windows-amd64-latest.exe

2. Generazione dei File di Migrazione Basati sulle Definizioni delle Entità Attuali

atlas migrate diff nome_migrazione \
  --dir "file://ent/migrate/migrations" \
  --to "ent://ent/schema" \
  --dev-url "docker://mysql/8/ent"

3. File di Migrazione dell'Applicazione

Una volta che i file di migrazione sono generati, possono essere applicati agli ambienti di sviluppo, test o produzione. Tipicamente, si applicherebbero prima questi file di migrazione a un database di sviluppo o di test per assicurarsi che vengano eseguiti come previsto. Successivamente, gli stessi passaggi di migrazione verrebbero eseguiti nell'ambiente di produzione.

atlas migrate apply \
  --dir "file://ent/migrate/migrations" \
  --url "mysql://root:pass@localhost:3306/example"

Usa il comando atlas migrate apply, specificando la directory dei file di migrazione (--dir) e l'URL del database di destinazione (--url) per applicare i file di migrazione.