1. Introduzione a ent

Ent è un framework di entità sviluppato da Facebook specificamente per il linguaggio Go. Semplifica il processo di costruzione e mantenimento di applicazioni di modelli di dati su larga scala. Il framework ent segue principalmente i seguenti principi:

  • Modellare facilmente lo schema del database come una struttura a grafo.
  • Definire lo schema sotto forma di codice del linguaggio Go.
  • Implementare tipi statici basati sulla generazione del codice.
  • Scrivere query al database e attraversamento del grafo è molto semplice.
  • Estendere e personalizzare facilmente usando i modelli Go.

2. Configurazione dell'ambiente

Per iniziare a utilizzare il framework ent, assicurati di avere installato il linguaggio Go nel tuo ambiente di sviluppo.

Se la directory del tuo progetto si trova al di fuori di GOPATH, o se non sei familiare con GOPATH, puoi utilizzare il seguente comando per creare un nuovo progetto modulo Go:

go mod init entdemo

Questo inizializzerà un nuovo modulo Go e creerà un nuovo file go.mod per il tuo progetto entdemo.

3. Definizione del primo schema

3.1. Creazione dello schema utilizzando ent CLI

Innanzitutto, è necessario eseguire il seguente comando nella directory principale del tuo progetto per creare uno schema denominato User utilizzando lo strumento ent CLI:

go run -mod=mod entgo.io/ent/cmd/ent new User

Il comando soprastante genererà lo schema User nella directory entdemo/ent/schema/:

File entdemo/ent/schema/user.go:

package schema

import "entgo.io/ent"

// User contiene la definizione dello schema per l'entità User.
type User struct {
    ent.Schema
}

// Campi dell'utente.
func (User) Fields() []ent.Field {
    return nil
}

// Bordini dell'utente.
func (User) Edges() []ent.Edge {
    return nil
}

3.2. Aggiunta di campi

Successivamente, è necessario aggiungere le definizioni dei campi allo Schema dell'utente. Di seguito è riportato un esempio di aggiunta di due campi all'entità User.

File modificato entdemo/ent/schema/user.go:

package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/schema/field"
)

// Campi dell'utente.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("name").
            Default("unknown"),
    }
}

Questo pezzo di codice definisce due campi per il modello utente: age e name, dove age è un numero intero positivo e name è una stringa con un valore predefinito di "unknown".

3.3. Generazione di entità del database

Dopo aver definito lo schema, è necessario eseguire il comando go generate per generare la logica di accesso al database sottostante.

Esegui il seguente comando nella directory principale del tuo progetto:

go generate ./ent

Questo comando genererà il corrispondente codice Go in base allo schema definito in precedenza, risultando nella seguente struttura dei file:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... (alcuni file omessi per brevità)
├── schema
│   └── user.go
├── tx.go
├── user
│   ├── user.go
│   └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go

4.1. Inizializzazione della connessione al database

Per stabilire una connessione al database MySQL, possiamo utilizzare la funzione Open fornita dal framework ent. Prima, importa il driver MySQL e poi fornisce la stringa di connessione corretta per inizializzare la connessione al database.

pacchetto principale

import (
    "contesto"
    "registrar"
    "entdemo/ent"
    
    _ "github.com/go-sql-driver/mysql" // Importa il driver MySQL
)

funzione principale() {
    // Utilizza ent.Open per stabilire una connessione con il database MySQL.
    // Ricorda di sostituire i segnaposto "tuo_nome_utente", "tua_password" e "tuo_database" di seguito.
    client, err := ent.Open("mysql", "tuo_nome_utente:tua_password@tcp(localhost:3306)/tuo_database?parseTime=True")
    se err != nil {
        log.Fatalf("apertura della connessione a mysql non riuscita: %v", err)
    }
    defer client.Close()
    
    // Esegui lo strumento di migrazione automatica
    ctx := contesto.Sfondo()
    se err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("creazione delle risorse dello schema non riuscita: %v", err)
    }
    
    // Qui è possibile scrivere ulteriore logica di business
}

4.2. Creazione di entità

Creare un'entità Utente comporta la costruzione di un nuovo oggetto entità e la persistenza nel database utilizzando il metodo Save o SaveX. Il seguente codice dimostra come creare una nuova entità Utente e inizializzare due campi età e nome.

// La funzione CreateUser viene utilizzata per creare una nuova entità Utente
funzione CreateUser(ctx contesto.Contesto, client *ent.Client) (*ent.User, errore) {
    // Utilizza client.User.Create() per costruire la richiesta di creazione di un Utente,
    // quindi concatenare i metodi SetAge e SetName per impostare i valori dei campi dell'entità.
    u, err := client.User.
        Create().
        SetAge(30).    // Imposta l'età dell'utente
        SetName("a8m"). // Imposta il nome dell'utente
        Save(ctx)     // Chiama Save per salvare l'entità nel database
    se err != nil {
        return nil, fmt.Errorf("creazione utente non riuscita: %w", err)
    }
    log.Println("utente è stato creato: ", u)
    return u, nil
}

Nella funzione principale, è possibile chiamare la funzione CreateUser per creare una nuova entità utente.

funzione principale() {
    // ...Codice omesso di creazione della connessione al database

    // Crea un'entità utente
    u, err := CreateUser(ctx, client)
    se err != nil {
        log.Fatalf("creazione utente non riuscita: %v", err)
    }
    log.Printf("utente creato: %#v\n", u)
}

4.3. Query delle Entità

Per interrogare le entità, possiamo utilizzare il generatore di query generato da ent. Il codice seguente dimostra come interrogare un utente di nome "a8m":

// La funzione QueryUser viene utilizzata per interrogare l'entità utente con un nome specificato
funzione QueryUser(ctx contesto.Contesto, client *ent.Client) (*ent.User, errore) {
    // Utilizza client.User.Query() per costruire la query per Utente,
    // quindi concatenare il metodo Where per aggiungere condizioni di query, come ad esempio interrogare per nome utente
    u, err := client.User.
        Query().
        Where(user.NameEQ("a8m")).      // Aggiungi la condizione di query, in questo caso, il nome è "a8m"
        Only(ctx)                      // Il metodo Only indica che è previsto solo un risultato
    se err != nil {
        return nil, fmt.Errorf("interrogazione utente non riuscita: %w", err)
    }
    log.Println("utente restituito: ", u)
    return u, nil
}

Nella funzione principale, è possibile chiamare la funzione QueryUser per interrogare l'entità utente.

funzione principale() {
    // ...Codice omesso di creazione della connessione al database e creazione dell'utente

    // Interroga l'entità utente
    u, err := QueryUser(ctx, client)
    se err != nil {
        log.Fatalf("interrogazione utente non riuscita: %v", err)
    }
    log.Printf("utente interrogato: %#v\n", u)
}

5.1. Comprensione dei bordi e dei bordi inversi

Nel framework ent, il modello di dati è visualizzato come una struttura a grafo, in cui le entità rappresentano nodi nel grafo, e le relazioni tra entità sono rappresentate dai bordi. Un bordo è una connessione da un'entità a un'altra, ad esempio, un Utente può possedere più Auto.

I bordi inversi sono riferimenti inversi ai bordi, rappresentando logicamente la relazione inversa tra le entità, ma senza creare una nuova relazione nel database. Ad esempio, attraverso il bordo inverso di un'Auto, possiamo trovare l'Utente che possiede questa auto.

La principale importanza dei bordi e dei bordi inversi sta nel rendere molto intuitiva e diretta la navigazione tra entità associate.

Suggerimento: In ent, i bordi corrispondono alle chiavi esterne del database tradizionale e vengono utilizzati per definire le relazioni tra tabelle.

5.2. Definizione dei bordi nello schema

Inizialmente, utilizzeremo la CLI di ent per creare lo schema iniziale per Auto e Gruppo:

go run -mod=mod entgo.io/ent/cmd/ent new Car Group

Successivamente, nello schema dell'entità Utente, definiamo il bordo con Auto per rappresentare la relazione tra utenti e auto. Possiamo aggiungere un bordo auto che punta al tipo Car nell'entità utente, indicando che un utente può avere più auto:

// entdemo/ent/schema/user.go

// Bordi dell'utente.
func (Utente) Bordi() []ent.Bordo {
    return []ent.Bordo{
        bordo.To("auto", Car.Type),
    }
}

Dopo aver definito i bordi, è necessario eseguire di nuovo go generate ./ent per generare il codice corrispondente.

5.3. Operare sui dati del bordo

Creare auto associate a un utente è un processo semplice. Data un'entità utente, possiamo creare una nuova entità auto e associarla all'utente:

import (
    "context"
    "log"
    "entdemo/ent"
    // Assicurati di importare la definizione dello schema per Auto
    _ "entdemo/ent/schema"
)

func CreaAutoPerUtente(ctx context.Context, client *ent.Client, IDUtente int) error {
    utente, err := client.Utente.Get(ctx, IDUtente)
    if err != nil {
        log.Fatalf("impossibile recuperare l'utente: %v", err)
        return err
    }

    // Crea una nuova auto e associarla all'utente
    _, err = client.Auto.
        Create().
        SetModello("Tesla").
        SetDataRegistrazione(time.Now()).
        SetProprietario(utente).
        Save(ctx)
    if err != nil {
        log.Fatalf("impossibile creare l'auto per l'utente: %v", err)
        return err
    }

    log.Println("auto è stata creata e associata all'utente")
    return nil
}

Analogamente, interrogare le auto di un utente è semplice. Se vogliamo recuperare un elenco di tutte le auto possedute da un utente, possiamo fare quanto segue:

func InterrogaAutoUtente(ctx context.Context, client *ent.Client, IDUtente int) error {
    utente, err := client.Utente.Get(ctx, IDUtente)
    if err != nil {
        log.Fatalf("impossibile recuperare l'utente: %v", err)
        return err
    }

    // Interroga tutte le auto di proprietà dell'utente
    auto, err := utente.QueryAuto().Tutte(ctx)
    if err != nil {
        log.Fatalf("interrogazione delle auto non riuscita: %v", err)
        return err
    }

    for _, auto := range auto {
        log.Printf("auto: %v, modello: %v", auto.ID, auto.Modello)
    }
    return nil
}

Attraverso i passaggi sopra, abbiamo non solo appreso come definire i bordi nello schema, ma anche dimostrato come creare ed interrogare dati relativi ai bordi.

6. Trasversalità del Grafo e Interrogazione

6.1. Comprensione delle Strutture a Grafo

In ent, le strutture a grafo sono rappresentate da entità e dai bordi tra esse. Ogni entità equivale a un nodo nel grafo, e le relazioni tra entità sono rappresentate dai bordi, che possono essere uno a uno, uno a molti, molti a molti, ecc. Questa struttura a grafo rende semplici ed intuitive le query e le operazioni complesse su un database relazionale.

6.2. Attraversamento delle strutture del Grafo

Scrivere il codice di attraversamento del grafo coinvolge principalmente l'interrogazione e l'associazione dei dati attraverso i collegamenti tra le entità. Di seguito è riportato un semplice esempio che mostra come attraversare la struttura del grafo in ent:

import (
    "context"
    "log"

    "entdemo/ent"
)

// GraphTraversal è un esempio di attraversamento della struttura del grafo
func GraphTraversal(ctx context.Context, client *ent.Client) error {
    // Interroga l'utente di nome "Ariel"
    a8m, err := client.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
    if err != nil {
        log.Fatalf("Errore nell'interrogazione dell'utente: %v", err)
        return err
    }

    // Attraversa tutte le auto appartenenti ad Ariel
    cars, err := a8m.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("Errore nell'interrogazione delle auto: %v", err)
        return err
    }
    for _, car := range cars {
        log.Printf("Ariel ha un'auto con modello: %s", car.Model)
    }

    // Attraversa tutti i gruppi di cui Ariel è membro
    groups, err := a8m.QueryGroups().All(ctx)
    if err != nil {
        log.Fatalf("Errore nell'interrogazione dei gruppi: %v", err)
        return err
    }
    for _, g := range groups {
        log.Printf("Ariel è membro del gruppo: %s", g.Name)
    }

    return nil
}

Il codice sopra è un esempio di base di attraversamento del grafo, che prima interroga un utente e poi attraversa le auto e i gruppi dell'utente.

7. Visualizzazione dello schema del Database

7.1. Installazione dello strumento Atlas

Per visualizzare lo schema del database generato da ent, possiamo utilizzare lo strumento Atlas. I passaggi di installazione per Atlas sono molto semplici. Ad esempio, su macOS, è possibile installarlo usando brew:

brew install ariga/tap/atlas

Nota: Atlas è uno strumento universale di migrazione del database che gestisce la gestione delle versioni della struttura della tabella per vari database. Una dettagliata introduzione ad Atlas sarà fornita nei capitoli successivi.

7.2. Generazione di ERD e Schema SQL

Utilizzare Atlas per visualizzare ed esportare gli schemi è molto semplice. Dopo aver installato Atlas, è possibile utilizzare il seguente comando per visualizzare il Diagramma Entità-Relazione (ERD):

atlas schema inspect -d [database_dsn] --format dot

O generare direttamente lo Schema SQL:

atlas schema inspect -d [database_dsn] --format sql

Dove [database_dsn] punta al nome della fonte dati (DSN) del tuo database. Ad esempio, per un database SQLite, potrebbe essere:

atlas schema inspect -d "sqlite://file:ent.db?mode=memory&cache=shared" --format dot

L'output generato da questi comandi può essere ulteriormente trasformato in visualizzazioni o documenti utilizzando gli strumenti rispettivi.

8. Migrazione dello Schema

8.1. Migrazione Automatica e Migrazione Versionata

ent supporta due strategie di migrazione dello schema: migrazione automatica e migrazione versionata. La migrazione automatica è il processo di ispezione e applicazione delle modifiche dello schema durante l'esecuzione, adatta per lo sviluppo e il testing. La migrazione versionata coinvolge la generazione di script di migrazione e richiede una revisione e un testing attenti prima della distribuzione in produzione.

Suggerimento: Per la migrazione automatica, fare riferimento al contenuto nella sezione 4.1.

8.2. Esecuzione della Migrazione Versionata

Il processo di migrazione versionata coinvolge la generazione di file di migrazione attraverso Atlas. Di seguito sono riportati i comandi pertinenti:

Per generare file di migrazione:

atlas migrate diff -d ent/schema/path --dir migrations/dir

Successivamente, questi file di migrazione possono essere applicati al database:

atlas migrate apply -d migrations/dir --url database_dsn

Seguendo questo processo, è possibile mantenere un registro delle migrazioni del database nel sistema di controllo delle versioni e garantire una revisione approfondita prima di ciascuna migrazione.

Suggerimento: Fare riferimento al codice di esempio su https://github.com/ent/ent/tree/master/examples/start