1. Concetti di Modello e Campo

1.1. Introduzione alla Definizione del Modello

In un framework ORM, un modello viene utilizzato per descrivere la relazione di mapping tra i tipi di entità nell'applicazione e le tabelle del database. Il modello definisce le proprietà e le relazioni dell'entità, nonché le configurazioni specifiche del database ad esse associate. Nel framework ent, i modelli vengono tipicamente utilizzati per descrivere i tipi di entità in un grafo, come Utente o Gruppo.

Le definizioni dei modelli includono tipicamente descrizioni dei campi (o proprietà) e degli edge (o relazioni) dell'entità, nonché alcune opzioni specifiche del database. Queste descrizioni possono aiutarci a definire la struttura, le proprietà e le relazioni dell'entità, e possono essere utilizzate per generare la corrispondente struttura della tabella del database basata sul modello.

1.2. Panoramica dei Campi

I campi sono la parte del modello che rappresenta le proprietà dell'entità. Essi definiscono le proprietà dell'entità, come nome, età, data, ecc. Nel framework ent, i tipi di campo includono vari tipi di dati di base, come intero, stringa, booleano, tempo, ecc., oltre ad alcuni tipi specifici di SQL, come UUID, []byte, JSON, ecc.

La tabella riportata di seguito mostra i tipi di campo supportati dal framework ent:

Tipo Descrizione
int Tipo intero
uint8 Tipo di intero non segnato a 8 bit
float64 Tipo di punto decimale
bool Tipo booleano
string Tipo stringa
time.Time Tipo tempo
UUID Tipo UUID
[]byte Tipo di array di byte (solo SQL)
JSON Tipo JSON (solo SQL)
Enum Tipo enumerato (solo SQL)
Altro Altri tipi (ad es., Intervallo Postgres)

2. Dettagli delle Proprietà dei Campi

2.1. Tipi di Dati

Il tipo di dati di un attributo o campo in un modello di entità determina la forma dei dati che possono essere memorizzati. Questa è una parte cruciale della definizione del modello nel framework ent. Di seguito sono riportati alcuni tipi di dati comunemente utilizzati nel framework ent.

import (
    "time"

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

// Schema di Utente.
type Utente struct {
    ent.Schema
}

// Campi di Utente.
func (Utente) Fields() []ent.Field {
    return []ent.Field{
        field.Int("età"),             // Tipo intero
        field.String("nome"),         // Tipo stringa
        field.Bool("attivo"),         // Tipo booleano
        field.Float("punteggio"),     // Tipo punto decimale
        field.Time("creato_il"),      // Tipo timestamp
    }
}
  • int: Rappresenta valori interi, che possono essere int8, int16, int32, int64, ecc.
  • string: Rappresenta dati di stringa.
  • bool: Rappresenta valori booleani, tipicamente utilizzati come flag.
  • float64: Rappresenta numeri in virgola mobile, può anche utilizzare float32.
  • time.Time: Rappresenta il tempo, tipicamente utilizzato per timestamp o dati di data.

Questi tipi di campo verranno mappati ai corrispondenti tipi supportati dal database sottostante. Inoltre, ent supporta tipi più complessi come UUID, JSON, enumerazioni (Enum), e supporta tipi di database speciali come []byte (solo SQL) e Altro (solo SQL).

2.2. Valori Predefiniti

I campi possono essere configurati con valori predefiniti. Se il valore corrispondente non è specificato durante la creazione di un'entità, verrà utilizzato il valore predefinito. Il valore predefinito può essere un valore fisso o un valore generato in modo dinamico da una funzione. Utilizzare il metodo .Default per impostare un valore predefinito statico, o utilizzare .DefaultFunc per impostare un valore predefinito generato in modo dinamico.

// Schema di Utente.
func (Utente) Fields() []ent.Field {
    return []ent.Field{
        field.Time("creato_il").
            Default(time.Now),  // Un valore predefinito fisso di time.Now
        field.String("ruolo").
            Default("utente"),   // Un valore di stringa costante
        field.Float("punteggio").
            DefaultFunc(func() float64 {
                return 10.0  // Un valore predefinito generato da una funzione
            }),
    }
}

2.3. Opzionalità dei campi e zeri

Per default, i campi sono obbligatori. Per dichiarare un campo opzionale, utilizzare il metodo .Optional(). I campi opzionali saranno dichiarati come campi nullable nel database. L'opzione Nillable consente ai campi di essere impostati esplicitamente a nil, distinguendo tra il valore zero di un campo e uno stato non impostato.

// Schema dell'utente.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("nickname").Optional(), // Il campo opzionale non è richiesto
        field.Int("age").Optional().Nillable(), // Il campo nillable può essere impostato a nil
    }
}

Utilizzando il modello definito sopra, il campo age può accettare sia valori nil per indicare uno stato non impostato, sia valori zero non nil.

2.4. Unicità dei campi

I campi univoci garantiscono che non ci siano valori duplicati nella tabella del database. Utilizzare il metodo Unique() per definire un campo univoco. Quando si stabilisce l'integrità dei dati come requisito critico, ad esempio per email o nomi utente degli utenti, dovrebbero essere utilizzati campi univoci.

// Schema dell'utente.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("email").Unique(),  // Campo univoco per evitare indirizzi email duplicati
    }
}

Ciò creerà un vincolo unico nel database sottostante per impedire l'inserimento di valori duplicati.

2.5. Indicizzazione dei campi

L'indicizzazione dei campi è utilizzata per migliorare le prestazioni delle query del database, specialmente in database di grandi dimensioni. Nel framework ent, il metodo .Indexes() può essere utilizzato per creare indicizzazioni.

import "entgo.io/ent/schema/index"

// Schema dell'utente.
func (User) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("email"),  // Crea un'indicizzazione sul campo 'email'
        index.Fields("name", "age").Unique(), // Crea un'indicizzazione composita univoca
    }
}

Le indicizzazioni possono essere utilizzate per i campi frequentemente interrogati, ma è importante notare che troppi indici possono portare a una diminuzione delle prestazioni delle operazioni di scrittura. Pertanto, la decisione di creare indicizzazioni dovrebbe essere bilanciata in base alle circostanze effettive.

2.6. Tag Personalizzati

Nel framework ent, è possibile utilizzare il metodo StructTag per aggiungere tag personalizzati ai campi della struttura dell'entità generata. Questi tag sono molto utili per implementare operazioni come la codifica JSON e la codifica XML. Nell'esempio di seguito, aggiungeremo tag JSON e XML personalizzati per il campo name.

// Campi dell'utente.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            // Aggiungi tag personalizzati utilizzando il metodo StructTag
            // Qui, impostare il tag JSON per il campo name su 'username' e ignorarlo quando il campo è vuoto (omitempty)
            // Inoltre, impostare il tag XML per la codifica su 'name'
            StructTag(`json:"username,omitempty" xml:"name"`),
    }
}

Nella codifica con JSON o XML, l'opzione omitempty indica che se il campo name è vuoto, allora questo campo verrà omesso dal risultato della codifica. Questo è molto utile per ridurre le dimensioni del corpo della risposta durante la scrittura di API.

Questo dimostra anche come impostare più tag per lo stesso campo contemporaneamente. I tag JSON utilizzano la chiave json, i tag XML utilizzano la chiave xml, e sono separati da spazi. Questi tag saranno utilizzati dalle funzioni di libreria come encoding/json e encoding/xml durante il parsing della struttura per la codifica o decodifica.

3. Validazione dei Campi e Vincoli

La validazione dei campi è un aspetto importante della progettazione del database per garantire la coerenza e la validità dei dati nel modello delle entità. In questa sezione, approfondiremo l'uso dei validatori incorporati, dei validatori personalizzati e vari vincoli per migliorare l'integrità e la qualità dei dati nel modello delle entità.

3.1. Validatori Integrati

Il framework fornisce una serie di validatori integrati per effettuare controlli comuni sulla validità dei dati su diversi tipi di campi. Utilizzare questi validatori integrati può semplificare il processo di sviluppo e definire rapidamente intervalli o formati validi per i campi.

Ecco alcuni esempi di validatori integrati per i campi:

  • Validatori per tipi numerici:
    • Positive(): Valida se il valore del campo è un numero positivo.
    • Negative(): Valida se il valore del campo è un numero negativo.
    • NonNegative(): Valida se il valore del campo è un numero non negativo.
    • Min(i): Valida se il valore del campo è maggiore di un dato valore minimo i.
    • Max(i): Valida se il valore del campo è minore di un dato valore massimo i.
  • Validatori per il tipo string:
    • MinLen(i): Valida la lunghezza minima di una stringa.
    • MaxLen(i): Valida la lunghezza massima di una stringa.
    • Match(regexp.Regexp): Valida se la stringa corrisponde all'espressione regolare data.
    • NotEmpty: Valida se la stringa non è vuota.

Diamo un'occhiata a un esempio pratico di codice. In questo esempio, viene creato un modello Utente, che include un campo di tipo intero non negativo età e un campo email con un formato fisso:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("email").
            Match(regexp.MustCompile(`^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$`)),
    }
}

3.2. Validatori Personalizzati

Mentre i validatori integrati possono gestire molti requisiti di convalida comuni, a volte potresti avere bisogno di logiche di convalida più complesse. In tali casi, puoi scrivere validatori personalizzati per soddisfare regole aziendali specifiche.

Un validatore personalizzato è una funzione che riceve un valore di campo e restituisce un error. Se l'error restituito non è vuoto, indica un fallimento della convalida. Il formato generale di un validatore personalizzato è il seguente:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("phone").
            Validate(func(s string) error {
                // Verifica se il numero di telefono ha il formato previsto
                corrisposto, _ := regexp.MatchString(`^\+?[1-9]\d{1,14}$`, s)
                if !corrisposto {
                    return errors.New("Formato numero di telefono non corretto")
                }
                return nil
            }),
    }
}

Come mostrato sopra, abbiamo creato un validatore personalizzato per convalidare il formato di un numero di telefono.

3.3. Vincoli

I vincoli sono regole che impongono regole specifiche su un oggetto del database. Possono essere utilizzati per garantire la correttezza e la coerenza dei dati, ad esempio impedendo l'inserimento di dati non validi o definendo le relazioni dei dati.

I vincoli del database comuni includono:

  • Vincolo di chiave primaria: Garantisce che ogni record nella tabella sia unico.
  • Vincolo unico: Garantisce che il valore di una colonna o una combinazione di colonne sia unico nella tabella.
  • Vincolo di chiave esterna: Definisce le relazioni tra tabelle, garantendo l'integrità referenziale.
  • Vincolo di controllo: Garantisce che un valore di campo soddisfi una condizione specifica.

Nel modello di entità, puoi definire vincoli per mantenere l'integrità dei dati come segue:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("username").
            Unique(), // Vincolo unico per garantire che lo username sia unico nella tabella.
        field.String("email").
            Unique(), // Vincolo unico per garantire che l'email sia unica nella tabella.
    }
}

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("friends", User.Type).
            Unique(), // Vincolo di chiave esterna, creando una relazione di bordo unica con un altro utente.
    }
}

In sintesi, la convalida dei campi e i vincoli sono cruciali per garantire una buona qualità dei dati e evitare errori di dati imprevisti. Utilizzare gli strumenti forniti dal framework ent può semplificare e rendere più affidabile questo processo.