1. Grundkonzepte von Entität und Assoziation

Im ent-Framework bezeichnet eine Entität die grundlegende Dateneinheit, die in der Datenbank verwaltet wird und in der Regel einer Tabelle in der Datenbank entspricht. Die Felder in der Entität entsprechen den Spalten in der Tabelle, während die Assoziationen (Kanten) zwischen Entitäten dazu dienen, die Beziehungen und Abhängigkeiten zwischen den Entitäten zu beschreiben. Entitäts-Assoziationen bilden die Grundlage für den Aufbau komplexer Datenmodelle und ermöglichen die Darstellung hierarchischer Beziehungen wie Eltern-Kind-Beziehungen und Besitzverhältnisse.

Das ent-Framework bietet eine umfangreiche Sammlung von APIs, die es Entwicklern ermöglichen, diese Assoziationen im Entitätsschema zu definieren und zu verwalten. Über diese Assoziationen können wir die komplexe Geschäftslogik zwischen Daten einfach darstellen und darauf zugreifen.

2. Arten von Entitäts-Assoziationen in ent

2.1 Eins-zu-Eins (O2O) Assoziation

Eine Eins-zu-Eins-Assoziation bezieht sich auf eine Eins-zu-Eins-Korrespondenz zwischen zwei Entitäten. Zum Beispiel kann im Fall von Benutzern und Bankkonten jeder Benutzer nur ein Bankkonto haben, und jedes Bankkonto gehört auch nur zu einem Benutzer. Das ent-Framework verwendet die Methoden edge.To und edge.From, um solche Assoziationen zu definieren.

Zunächst können wir eine Eins-zu-Eins-Assoziation, die auf Card verweist, im Schema von User definieren:

// Kanten des Benutzers.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("card", Card.Type). // Zeigt auf die Entität Card und definiert den Assoziationsnamen als "card"
            Unique(),               // Die Methode Unique stellt sicher, dass dies eine Eins-zu-Eins-Assoziation ist
    }
}

Anschließend definieren wir die umgekehrte Assoziation zurück zum Benutzer im Schema von Card:

// Kanten der Karte.
func (Card) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Zeigt von der Karte zurück zum Benutzer und definiert den Assoziationsnamen als "owner"
            Ref("card").              // Die Methode Ref spezifiziert den entsprechenden umgekehrten Assoziationsnamen
            Unique(),                 // Als eindeutig markiert, um sicherzustellen, dass eine Karte einem Besitzer entspricht
    }
}

2.2 Eins-zu-Viele (O2M) Assoziation

Eine Eins-zu-Viele-Assoziation zeigt an, dass eine Entität mit mehreren anderen Entitäten verbunden sein kann, diese Entitäten jedoch nur auf eine einzige Entität zurückverweisen können. Zum Beispiel kann ein Benutzer mehrere Haustiere haben, aber jedes Haustier hat nur einen Besitzer.

In ent verwenden wir immer noch edge.To und edge.From, um diese Art von Assoziation zu definieren. Im folgenden Beispiel wird eine Eins-zu-Viele-Assoziation zwischen Benutzern und Haustieren definiert:

// Kanten des Benutzers.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("pets", Pet.Type), // Eins-zu-Viele-Assoziation vom Benutzer zur Entität Pet
    }
}

In der Entität Pet definieren wir eine Viele-zu-Eins-Assoziation zurück zum Benutzer:

// Kanten des Haustiers.
func (Pet) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Viele-zu-Eins-Assoziation vom Haustier zum Benutzer
            Ref("pets").              // Spezifiziert den umgekehrten Assoziationsnamen vom Haustier zum Besitzer
            Unique(),                 // Stellt sicher, dass ein Besitzer mehrere Haustiere haben kann
    }
}

2.3 Viele-zu-Viele (M2M) Assoziation

Eine Viele-zu-Viele-Assoziation ermöglicht es zwei Arten von Entitäten, mehrere Instanzen voneinander zu haben. Zum Beispiel kann sich ein Student für mehrere Kurse einschreiben, und ein Kurs kann auch mehrere eingeschriebene Studenten haben. ent bietet eine API zum Aufbau von Viele-zu-Viele-Assoziationen:

In der Entität Student verwenden wir edge.To, um eine Viele-zu-Viele-Assoziation mit Course zu etablieren:

// Kanten des Studenten.
func (Student) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("courses", Course.Type), // Definiert eine Viele-zu-Viele-Assoziation vom Studenten zum Kurs
    }
}

Ebenso definieren wir in der Entität Course eine umgekehrte Assoziation zu Student für die Viele-zu-Viele-Beziehung:

// Kanten des Kurses.
func (Course) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("students", Student.Type). // Definiert eine Viele-zu-Viele-Assoziation vom Kurs zum Studenten
            Ref("courses"),                  // Spezifiziert den umgekehrten Assoziationsnamen vom Kurs zum Studenten
    }
}

Diese Arten von Assoziationen bilden das Fundament für den Aufbau komplexer Anwendungsdatenmodelle, und das Verständnis, wie man sie in ent definiert und verwendet, ist entscheidend für die Erweiterung von Datenmodellen und Geschäftslogik.

3. Grundlegende Operationen für Entitätsverknüpfungen

In diesem Abschnitt wird erläutert, wie grundlegende Operationen mit ent unter Verwendung der definierten Beziehungen durchgeführt werden, einschließlich Erstellung, Abfrage und Durchführung von verknüpften Entitäten.

3.1 Erstellen von verknüpften Entitäten

Bei der Erstellung von Entitäten können Sie gleichzeitig die Beziehungen zwischen den Entitäten festlegen. Für Einer-zu-Vielen (O2M) und Viele-zu-Vielen (M2M) Beziehungen können Sie die Add{Edge}-Methode verwenden, um verknüpfte Entitäten hinzuzufügen.

Wenn wir beispielsweise eine Benutzerentität und eine Haustierentität mit einer bestimmten Verknüpfung haben, bei der ein Benutzer mehrere Haustiere haben kann, ist dies ein Beispiel für die Erstellung eines neuen Benutzers und hinzufügen von Haustieren für sie:

// Einen Benutzer erstellen und Haustiere hinzufügen
func CreateUserWithPets(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Eine Haustierinstanz erstellen
    fido := client.Pet.
        Create().  
        SetName("Fido").
        SaveX(ctx)
    // Eine Benutzerinstanz erstellen und mit dem Haustier verknüpfen
    user := client.User.
        Create().
        SetName("Alice").
        AddPets(fido). // Verwenden Sie die AddPets-Methode, um das Haustier zu verknüpfen
        SaveX(ctx)

    return user, nil
}

In diesem Beispiel erstellen wir zunächst eine Haustierinstanz mit dem Namen Fido, erstellen dann einen Benutzer namens Alice und verknüpfen die Haustierinstanz mit dem Benutzer mithilfe der AddPets-Methode.

3.2 Abfragen von verknüpften Entitäten

Das Abfragen von verknüpften Entitäten ist eine gängige Operation in ent. Zum Beispiel können Sie die Query{Edge}-Methode verwenden, um andere mit einer bestimmten Entität verknüpfte Entitäten abzurufen.

Um bei unserem Beispiel von Benutzern und Haustieren zu bleiben, hier ist, wie Sie alle Haustiere abfragen, die einem Benutzer gehören:

// Alle Haustiere eines Benutzers abfragen
func QueryUserPets(ctx context.Context, client *ent.Client, userID int) ([]*ent.Pet, error) {
    pets, err := client.User.
        Get(ctx, userID). // Holen Sie die Benutzerinstanz anhand der Benutzer-ID
        QueryPets().      // Abfragen der mit dem Benutzer verknüpften Haustierentitäten
        All(ctx)          // Alle abgefragten Haustierentitäten zurückgeben
    if err != nil {
        return nil, err
    }

    return pets, nil
}

In obigem Codeausschnitt holen wir zunächst die Benutzerinstanz anhand der Benutzer-ID und rufen dann die QueryPets-Methode auf, um alle mit diesem Benutzer verknüpften Haustierentitäten abzurufen.

Hinweis: Das Codegenerierungstool von ent generiert automatisch die API für Abfrageoperationen basierend auf den definierten Entitätsbeziehungen. Es wird empfohlen, den generierten Code zu überprüfen.

4. Vorausladen (Eager Loading)

4.1 Prinzipien des Vorausladens

Vorausladen ist eine Technik, die beim Abfragen von Datenbanken verwendet wird, um im Voraus verbundene Entitäten abzurufen und zu laden. Dieser Ansatz wird häufig angewendet, um Daten, die mit mehreren Entitäten verbunden sind, in einem einzigen Schritt zu erhalten, um mehrere Datenbankabfragen in der nachfolgenden Verarbeitung zu vermeiden und somit die Leistung der Anwendung erheblich zu verbessern.

Im Ent-Framework wird das Vorausladen hauptsächlich verwendet, um Beziehungen zwischen Entitäten wie Eins-zu-Viele und Viele-zu-Viele zu behandeln. Beim Abrufen einer Entität aus der Datenbank werden die verknüpften Entitäten nicht automatisch geladen. Stattdessen werden sie bei Bedarf explizit durch das Vorausladen geladen. Dies ist entscheidend, um das N+1-Abfrageproblem (d. h. separate Abfragen für jede Elternentität) zu entlasten.

Im Ent-Framework wird das Vorausladen durch Verwendung der With-Methode im Abfrage-Builder realisiert. Diese Methode generiert entsprechende With...-Funktionen für jede Verknüpfung wie WithGroups und WithPets. Diese Methoden werden automatisch vom Ent-Framework generiert, und Programmierer können sie verwenden, um das spezifische Vorausladen von Verknüpfungen anzufordern.

Das Arbeitsprinzip des Vorausladens von Entitäten besteht darin, dass beim Abfragen der primären Entität ent zusätzliche Abfragen ausgeführt wird, um alle verknüpften Entitäten abzurufen. Anschließend werden diese Entitäten in das Edges-Feld des zurückgegebenen Objekts eingefügt. Dies bedeutet, dass ent möglicherweise mehrere Datenbankabfragen ausführt, mindestens einmal für jede verknüpfte Verknüpfung, die vorausgeladen werden muss. Obwohl diese Methode in bestimmten Szenarien möglicherweise weniger effizient ist als eine einzelne komplexe JOIN-Abfrage, bietet sie eine größere Flexibilität und wird voraussichtlich in zukünftigen Versionen von ent Leistungsoptimierungen erhalten.

4.2 Implementierung des Vorausladens

Wir werden nun demonstrieren, wie Vorausladeoperationen im Ent-Framework mithilfe einiger Beispielcodes durchgeführt werden, wobei die Modelle von Benutzern und Haustieren aus der Übersicht verwendet werden.

Vorabladen einer einzelnen Assoziation

Angenommen, wir möchten alle Benutzer aus der Datenbank abrufen und die Haustierdaten vorabladen. Dies kann erreicht werden, indem wir den folgenden Code schreiben:

users, err := client.User.
    Query().
    WithPets().
    All(ctx)
if err != nil {
    // Fehlerbehandlung
    return err
}
for _, u := range users {
    for _, p := range u.Edges.Pets {
        fmt.Printf("Benutzer (%v) besitzt Haustier (%v)\n", u.ID, p.ID)
    }
}

In diesem Beispiel verwenden wir die Methode WithPets, um ent zu bitten, die mit den Benutzern verknüpften Haustierentitäten vorabzuladen. Die vorabgeladenen Haustierdaten werden in das Feld Edges.Pets eingefügt, aus dem wir auf diese zugehörigen Daten zugreifen können.

Vorabladen mehrerer Assoziationen

ent ermöglicht es uns, gleichzeitig mehrere Assoziationen vorabzuladen und sogar vorab verschachtelte Assoziationen, Filterung, Sortierung oder die Begrenzung der Anzahl der vorabgeladenen Ergebnisse anzugeben. Nachstehend ein Beispiel für das Vorabladen der Haustiere von Administratoren und der Teams, denen sie angehören, wobei auch die Benutzer, die den Teams zugeordnet sind, vorabgeladen werden:

admins, err := client.User.
    Query().
    Where(user.Admin(true)).
    WithPets().
    WithGroups(func(q *ent.GroupQuery) {
        q.Limit(5)          // Begrenzung auf die ersten 5 Teams
        q.Order(ent.Asc(group.FieldName)) // Aufsteigende Sortierung nach Teamnamen
        q.WithUsers()       // Vorabladen der Benutzer im Team
    }).
    All(ctx)
if err != nil {
    // Fehler behandeln
    return err
}
for _, admin := range admins {
    for _, p := range admin.Edges.Pets {
        fmt.Printf("Admin (%v) besitzt Haustier (%v)\n", admin.ID, p.ID)
    }
    for _, g := range admin.Edges.Groups {
        fmt.Printf("Admin (%v) gehört zum Team (%v)\n", admin.ID, g.ID)
        for _, u := range g.Edges.Users {
            fmt.Printf("Team (%v) hat Mitglied (%v)\n", g.ID, u.ID)
        }
    }
}

Durch dieses Beispiel wird deutlich, wie leistungsstark und flexibel ent ist. Mit nur wenigen einfachen Methodenaufrufen kann es umfangreiche zugehörige Daten vorabladen und strukturiert organisieren. Dies bietet große Bequemlichkeit bei der Entwicklung datengesteuerter Anwendungen.