1. Concetti di Base di Entità e Associazione

Nel framework ent, un'entità si riferisce all'unità di dati di base gestita nel database, che di solito corrisponde a una tabella nel database. I campi dell'entità corrispondono alle colonne della tabella, mentre le associazioni (bordi) tra entità vengono utilizzate per descrivere le relazioni e le dipendenze tra entità. Le associazioni delle entità costituiscono la base per la costruzione di modelli dati complessi, consentendo la rappresentazione di relazioni gerarchiche come relazioni genitore-figlio e relazioni di proprietà.

Il framework ent fornisce un ricco insieme di API, consentendo agli sviluppatori di definire e gestire queste associazioni nello schema dell'entità. Attraverso queste associazioni, possiamo facilmente esprimere e operare sulla logica aziendale complessa dei dati.

2. Tipi di Associazioni delle Entità in ent

2.1 Associazione Uno a Uno (O2O)

Un'associazione uno a uno si riferisce a una corrispondenza uno a uno tra due entità. Ad esempio, nel caso degli utenti e dei conti bancari, ogni utente può avere solo un conto bancario, e anche ogni conto bancario appartiene solo a un utente. Il framework ent utilizza i metodi edge.To e edge.From per definire tali associazioni.

Innanzitutto, possiamo definire un'associazione uno a uno che punta a Card all'interno dello schema User:

// Bordi dell'Utente.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("card", Card.Type). // Punta all'entità Card, definendo il nome dell'associazione come "card"
            Unique(),               // Il metodo Unique assicura che si tratti di un'associazione uno a uno
    }
}

Successivamente, definiamo l'associazione inversa tornando a User all'interno dello schema Card:

// Bordi della Card.
func (Card) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Punta a User da Card, definendo il nome dell'associazione come "owner"
            Ref("card").               // Il metodo Ref specifica il nome dell'associazione inversa corrispondente
            Unique(),                  // Contrassegnato come unico per garantire che una carta corrisponda a un unico proprietario
    }
}

2.2 Associazione Uno a Molti (O2M)

Un'associazione uno a molti indica che un'entità può essere associata a più altre entità, ma queste entità possono solo puntare a una singola entità. Ad esempio, un utente può avere più animali domestici, ma ogni animale domestico ha solo un proprietario.

In ent, utilizziamo ancora edge.To e edge.From per definire questo tipo di associazione. L'esempio seguente definisce un'associazione uno a molti tra utenti e animali domestici:

// Bordi dell'Utente.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("pets", Pet.Type), // Associazione uno a molti dall'entità User all'entità Pet
    }
}

Nell'entità Pet, definiamo un'associazione molti a uno tornando a User:

// Bordi del Pet.
func (Pet) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Associazione molti a uno da Pet a User
            Ref("pets").               // Specifica il nome dell'associazione inversa da pet a proprietario
            Unique(),                  // Garantisce che un proprietario possa avere più animali domestici
    }
}

2.3 Associazione Molti a Molti (M2M)

Un'associazione molti a molti permette a due tipi di entità di avere più istanze l'una dell'altra. Ad esempio, uno studente può iscriversi a più corsi, e anche un corso può avere più studenti iscritti. ent fornisce un'API per stabilire associazioni molti a molti:

Nell'entità Student, utilizziamo edge.To per stabilire un'associazione molti a molti con Course:

// Bordi dello Student.
func (Student) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("courses", Course.Type), // Definire un'associazione molti a molti da Student a Course
    }
}

Allo stesso modo, nell'entità Course, stabiliamo un'associazione inversa a Student per la relazione molti a molti:

// Bordi del Course.
func (Course) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("students", Student.Type). // Definire un'associazione molti a molti da Course a Student
            Ref("courses"),                  // Specificare il nome dell'associazione inversa da Course a Student
    }
}

Questi tipi di associazioni sono il fondamento per la costruzione di modelli dati applicativi complessi, e comprendere come definirli e usarli in ent è cruciale per estendere i modelli dati e la logica aziendale.

3. Operazioni di base per le associazioni tra entità

Questa sezione illustrerà come eseguire operazioni di base utilizzando ent con le relazioni definite, inclusa la creazione, l'interrogazione e il travaso di entità associate.

3.1 Creazione di entità associate

Quando si creano entità, è possibile impostare contemporaneamente le relazioni tra le entità. Per relazioni uno a molti (O2M) e molti a molti (M2M), è possibile utilizzare il metodo Add{Edge} per aggiungere entità associate.

Ad esempio, se abbiamo un'entità utente e un'entità animale domestico con una certa associazione, dove un utente può avere più animali domestici, di seguito è riportato un esempio di creazione di un nuovo utente e aggiunta di animali domestici per loro:

// Crea un utente e aggiungi animali domestici
func CreateUserWithPets(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Crea un'istanza di animale domestico
    fido := client.Pet.
        Create().  
        SetName("Fido").
        SaveX(ctx)
    // Crea un'istanza di utente e associarla all'animale domestico
    user := client.User.
        Create().
        SetName("Alice").
        AddPets(fido). // Usa il metodo AddPets per associare l'animale domestico
        SaveX(ctx)

    return user, nil
}

In questo esempio, prima creiamo un'istanza di animale domestico chiamata Fido, quindi creiamo un utente chiamato Alice e associamo l'istanza di animale domestico all'utente utilizzando il metodo AddPets.

3.2 Interrogazione delle entità associate

L'interrogazione delle entità associate è un'operazione comune in ent. Ad esempio, è possibile utilizzare il metodo Query{Edge} per recuperare altre entità associate a una specifica entità.

Continuando con il nostro esempio di utenti e animali domestici, ecco come interrogare tutti gli animali domestici di proprietà di un utente:

// Interroga tutti gli animali domestici di un utente
func QueryUserPets(ctx context.Context, client *ent.Client, userID int) ([]*ent.Pet, error) {
    pets, err := client.User.
        Get(ctx, userID). // Ottieni l'istanza dell'utente in base all'ID dell'utente
        QueryPets().      // Interroga le entità animali domestici associate all'utente
        All(ctx)          // Restituisci tutte le entità animali domestici interrogate
    if err != nil {
        return nil, err
    }

    return pets, nil
}

Nel frammento di codice sopra, otteniamo prima l'istanza dell'utente in base all'ID dell'utente, quindi chiamiamo il metodo QueryPets per recuperare tutte le entità animali associate a quell'utente.

Nota: Lo strumento di generazione del codice di ent genera automaticamente l'API per le interrogazioni di associazione in base alle relazioni delle entità definite. Si consiglia di esaminare il codice generato.

4. Caricamento anticipato

4.1 Principi del caricamento anticipato

Il caricamento anticipato è una tecnica utilizzata nell'interrogazione dei database per recuperare e caricare entità associate in anticipo. Questo approccio è comunemente impiegato per ottenere dati relativi a più entità in un'unica operazione, al fine di evitare molteplici operazioni di interrogazione del database nel successivo processo, migliorando significativamente le prestazioni dell'applicazione.

Nel framework ent, il caricamento anticipato è principalmente utilizzato per gestire le relazioni tra entità, come uno a molti e molti a molti. Quando si recupera un'entità dal database, le entità associate ad essa non vengono caricate automaticamente. Invece, vengono esplicitamente caricate all'occorrenza tramite il caricamento anticipato. Questo è fondamentale per alleviare il problema della query N+1 (ossia, eseguire query separate per ciascuna entità genitore).

Nel framework ent, il caricamento anticipato è realizzato utilizzando il metodo With nel generatore di query. Questo metodo genera funzioni With... corrispondenti per ciascun edge, come WithGroups e WithPets. Questi metodi sono generati automaticamente dal framework ent e i programmatori possono utilizzarli per richiedere il caricamento anticipato di associazioni specifiche.

Il principio di funzionamento delle entità di caricamento anticipato è che quando si interroga l'entità primaria, ent esegue query aggiuntive per recuperare tutte le entità associate. Successivamente, queste entità vengono popolate nel campo Edges dell'oggetto restituito. Ciò significa che ent potrebbe eseguire query multiple al database, almeno una volta per ciascun edge associato che deve essere caricato anticipatamente. Sebbene questo metodo possa essere meno efficiente di una singola query JOIN complessa in certi scenari, offre maggiore flessibilità e si prevede che riceva ottimizzazioni delle prestazioni nelle versioni future di ent.

4.2 Implementazione del caricamento anticipato

Dimostreremo ora come eseguire operazioni di caricamento anticipato nel framework ent attraverso del codice di esempio, utilizzando i modelli di utenti e animali domestici illustrati nella panoramica.

Caricamento anticipato di un'associazione singola

Supponiamo che vogliamo recuperare tutti gli utenti dal database e caricare anticipatamente i dati relativi agli animali domestici. Possiamo ottenere ciò scrivendo il seguente codice:

users, err := client.User.
    Query().
    WithPets().
    All(ctx)
if err != nil {
    // Gestire l'errore
    return err
}
for _, u := range users {
    for _, p := range u.Edges.Pets {
        fmt.Printf("L'utente (%v) possiede l'animale domestico (%v)\n", u.ID, p.ID)
    }
}

In questo esempio, utilizziamo il metodo WithPets per richiedere a "ent" di caricare preventivamente le entità degli animali domestici associate agli utenti. I dati relativi agli animali domestici precaricati vengono popolati nel campo Edges.Pets, da cui è possibile accedere a questi dati associati.

Caricamento anticipato di più associazioni

"ent" ci consente di caricare preventivamente più associazioni in una volta, e persino specificare il caricamento anticipato di associazioni nidificate, filtrare, ordinare o limitare il numero di risultati precaricati. Di seguito è riportato un esempio del caricamento anticipato degli animali domestici degli amministratori e dei team a cui appartengono, mentre viene caricato preventivamente anche gli utenti associati ai team:

admins, err := client.User.
    Query().
    Where(user.Admin(true)).
    WithPets().
    WithGroups(func(q *ent.GroupQuery) {
        q.Limit(5)          // Limitare ai primi 5 team
        q.Order(ent.Asc(group.FieldName)) // Ordinare in ordine crescente per nome del team
        q.WithUsers()       // Caricare preventivamente gli utenti nel team
    }).
    All(ctx)
if err != nil {
    // Gestire gli errori
    return err
}
for _, admin := range admins {
    for _, p := range admin.Edges.Pets {
        fmt.Printf("L'amministratore (%v) possiede l'animale domestico (%v)\n", admin.ID, p.ID)
    }
    for _, g := range admin.Edges.Groups {
        fmt.Printf("L'amministratore (%v) appartiene al team (%v)\n", admin.ID, g.ID)
        for _, u := range g.Edges.Users {
            fmt.Printf("Il team (%v) ha il membro (%v)\n", g.ID, u.ID)
        }
    }
}

Attraverso questo esempio, è possibile osservare quanto sia potente e flessibile "ent". Con poche semplici chiamate di metodo, può caricare in modo preventivo dati associati ricchi e organizzarli in modo strutturato. Ciò fornisce grande comodità nello sviluppo di applicazioni basate sui dati.