1. Installation des ent-Tools

Um das Code-Generierungstool ent zu installieren, müssen Sie diese Schritte befolgen:

Stellen Sie zunächst sicher, dass Ihr System über die Go-Sprachumgebung verfügt. Führen Sie dann den folgenden Befehl aus, um das ent-Tool zu erhalten:

go get -d entgo.io/ent/cmd/ent

Dieser Befehl lädt den Code für das ent-Tool herunter, kompiliert und installiert es jedoch nicht sofort. Wenn Sie ent im Verzeichnis $GOPATH/bin installieren möchten, damit Sie es überall verwenden können, müssen Sie außerdem den folgenden Befehl ausführen:

go install entgo.io/ent/cmd/ent

Nach Abschluss der Installation können Sie überprüfen, ob das ent-Tool ordnungsgemäß installiert ist, und die verfügbaren Befehle und Anweisungen anzeigen, indem Sie ent -h ausführen.

2. Initialisierung des Schemas

2.1 Initialisieren der Vorlage mit ent init

Das Erstellen einer neuen Schemadatei ist der erste Schritt, um ent für die Code-Generierung zu verwenden. Sie können die Schema-Vorlage initialisieren, indem Sie den folgenden Befehl ausführen:

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

Dieser Befehl erstellt zwei neue Schemadateien: user.go und pet.go und platziert sie im Verzeichnis ent/schema. Falls das Verzeichnis ent noch nicht existiert, wird dieses automatisch erstellt.

Das Ausführen des Befehls ent init im Stammverzeichnis des Projekts ist eine gute Praxis, da es dazu beiträgt, die Struktur und Klarheit des Projektverzeichnisses aufrechtzuerhalten.

2.2 Struktur der Schemadatei

In dem Verzeichnis ent/schema entspricht jedes Schema einer Go-Sprachquelle. In den Schemadateien definieren Sie das Datenbankmodell, einschließlich der Felder und Kanten (Beziehungen).

Beispielsweise könnten Sie in der Datei user.go ein Benutzermodell definieren, einschließlich Feldern wie Benutzername und Alter, und die Beziehung zwischen Benutzern und Haustieren definieren. Ebenso würden Sie in der Datei pet.go das Haustiermodell und seine zugehörigen Felder definieren, wie beispielsweise den Namen des Haustiers, den Typ und die Beziehung zwischen Haustieren und Benutzern.

Diese Dateien werden letztendlich vom ent-Tool verwendet, um entsprechenden Go-Code zu generieren, einschließlich Client-Code für Datenbankoperationen und CRUD-Operationen (Create, Read, Update, Delete).

// ent/schema/user.go
package schema

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

// User definiert das Schema für die Benutzerentität.
type User struct {
    ent.Schema
}

// Die Fields-Methode wird verwendet, um die Felder der Entität zu definieren.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").Unique(),
        field.Int("age").Positive(),
    }
}

// Die Edges-Methode wird verwendet, um die Beziehungen der Entität zu definieren.
func (User) Edges() []ent.Edge {
    // Beziehungen werde im nächsten Abschnitt näher erläutert.
}

ent's Schemadateien verwenden Go-Sprachtys und -funktionen, um die Struktur des Datenbankmodells zu deklarieren, einschließlich der Felder und Beziehungen zwischen Modellen, und verwenden die API des ent-Frameworks, um diese Strukturen zu definieren. Dieser Ansatz macht ent sehr intuitiv und leicht erweiterbar, während er gleichzeitig von der starken Typisierung der Go-Sprache profitiert.

3.1 Code-Generierung ausführen

Die Ausführung von ent generate zur Code-Generierung ist ein entscheidender Schritt im ent-Framework. Mit diesem Befehl generiert ent entsprechende Go-Code-Dateien basierend auf den definierten Schemas, was die anschließende Entwicklungsarbeit erleichtert. Der Befehl zur Ausführung der Code-Generierung ist unkompliziert:

go generate ./ent

Der obige Befehl muss im Stammverzeichnis des Projekts ausgeführt werden. Wenn go generate aufgerufen wird, sucht es nach allen Go-Dateien, die spezifische Annotationen enthalten, und führt die in den Annotationen angegebenen Befehle aus. In unserem Beispiel legt dieser Befehl den Codegenerator für ent fest.

Stellen Sie sicher, dass die Initialisierung des Schemas und die erforderlichen Feldzugänge abgeschlossen sind, bevor Sie die Ausführung starten. Nur dann werden alle notwendigen Teile im generierten Code enthalten sein.

3.2 Verstehen der generierten Code-Ressourcen

Die generierten Code-Ressourcen enthalten mehrere Komponenten, von denen jede unterschiedliche Funktionen hat:

  • Client- und Tx-Objekte: Werden verwendet, um mit dem Datengraphen zu interagieren. Der Client bietet Methoden zum Erstellen von Transaktionen (Tx) oder direkt zur Ausführung von Datenbankoperationen.

  • CRUD-Builder: Generiert für jeden Schematyp Builder zum Erstellen, Lesen, Aktualisieren und Löschen und vereinfacht damit die Betriebslogik der entsprechenden Entität.

  • Entity-Objekt (Go-Struktur): Generiert die entsprechenden Go-Strukturen für jeden Typ im Schema und ordnet diese Strukturen den Tabellen in der Datenbank zu.

  • Konstanten und Prädikate-Paket: Enthält Konstanten und Prädikate zur Interaktion mit den Buildern.

  • Migrate-Paket: Ein Paket für die Datenbankmigration, das für SQL-Dialekte geeignet ist.

  • Hook-Paket: Bietet die Möglichkeit, Change-Middleware hinzuzufügen, die die Ausführung von benutzerdefinierter Logik vor oder nach dem Erstellen, Aktualisieren oder Löschen von Entitäten ermöglicht.

Durch die Untersuchung des generierten Codes können Sie ein tieferes Verständnis dafür erlangen, wie das ent-Framework den Datenzugriffscode für Ihre Schemata automatisiert.

4. Optionen zur Codegenerierung

Der Befehl ent generate unterstützt verschiedene Optionen, um den Codegenerierungsprozess anzupassen. Sie können alle unterstützten Generierungsoptionen mit dem folgenden Befehl abfragen:

ent generate -h

Hier sind einige häufig verwendete Befehlszeilenoptionen:

  • --feature strings: Erweitert die Codegenerierung um zusätzliche Funktionalitäten.
  • --header string: Überschreibt die Header-Datei der Codegenerierung.
  • --storage string: Gibt den im Codegenerierung unterstützten Speichertreiber an, der standardmäßig auf "sql" eingestellt ist.
  • --target string: Gibt das Zielverzeichnis für die Codegenerierung an.
  • --template strings: Führt zusätzliche Go-Templates aus. Unterstützt Datei-, Verzeichnis- und Wildcard-Pfade, z. B.: --template file="pfad/zur/datei.tmpl".

Diese Optionen ermöglichen es Entwicklern, ihren Codegenerierungsprozess entsprechend unterschiedlicher Anforderungen und Vorlieben anzupassen.

5. Konfiguration der Speicheroptionen

ent unterstützt die Generierung von Code-Ressourcen sowohl für SQL- als auch für Gremlin-Dialekte, wobei der SQL-Dialekt standardmäßig ist. Wenn das Projekt eine Verbindung zu einer Gremlin-Datenbank herstellen muss, müssen der entsprechende Datenbankdialect konfiguriert werden. Das Folgende zeigt, wie die Speicheroptionen angegeben werden:

ent generate --storage gremlin ./ent/schema

Der obige Befehl weist ent an, den Gremlin-Dialekt bei der Codegenerierung zu verwenden. Dadurch werden die generierten Ressourcen auf die Anforderungen der Gremlin-Datenbank zugeschnitten und die Kompatibilität mit einer spezifischen Graphdatenbank sichergestellt.

6. Erweiterte Verwendung: entc-Paket

6.1 Verwendung von entc als Paket im Projekt

entc ist das Kernpaket, das für die Codegenerierung im ent-Framework verwendet wird. Neben dem Befehlszeilentool kann entc auch als Paket in das Projekt integriert werden, um Entwicklern zu ermöglichen, den Codegenerierungsprozess innerhalb des Codes zu steuern und anzupassen.

Um entc als Paket im Projekt zu verwenden, müssen Sie eine Datei namens entc.go erstellen und folgenden Inhalt hinzufügen:

// +build ignore

package main

import (
    "log"
    "entgo.io/ent/entc"
    "entgo.io/ent/entc/gen"
)

func main() {
    if err := entc.Generate("./schema", &gen.Config{}); err != nil {
        log.Fatal("Fehler beim Ausführen der Ent-Codegenerierung:", err)
    }
}

Bei Verwendung dieses Ansatzes können Sie die gen.Config-Struktur innerhalb der main-Funktion modifizieren, um unterschiedliche Konfigurationsoptionen anzuwenden. Durch das Aufrufen der entc.Generate-Funktion nach Bedarf können Sie den Codegenerierungsprozess flexibel steuern.

6.2 Detaillierte Konfiguration von entc

entc bietet umfangreiche Konfigurationsoptionen, mit denen Entwickler den generierten Code anpassen können. Zum Beispiel können benutzerdefinierte Hooks konfiguriert werden, um den generierten Code zu inspizieren oder zu ändern, und externe Abhängigkeiten können mithilfe von Dependency Injection eingefügt werden.

Das folgende Beispiel zeigt, wie benutzerdefinierte Hooks für die entc.Generate-Funktion bereitgestellt werden können:

func main() {
    err := entc.Generate("./schema", &gen.Config{
        Hooks: []gen.Hook{
            HookFunction,
        },
    })
    if err != nil {
        log.Fatalf("ent-Codegenerierung fehlgeschlagen: %v", err)
    }
}

// HookFunction ist eine benutzerdefinierte Hook-Funktion
func HookFunction(next gen.Generator) gen.Generator {
    return gen.GenerateFunc(func(g *gen.Graph) error {
        // Hier kann der durch g dargestellte Graphmodus behandelt werden
        // Zum Beispiel die Validierung der Existenz von Feldern oder die Änderung der Struktur
        return next.Generate(g)
    })
}

Zusätzlich können externe Abhängigkeiten mithilfe von entc.Dependency hinzugefügt werden:

func main() {
    opts := []entc.Option{
        entc.Dependency(
            entc.DependencyType(&http.Client{}),
        ),
        entc.Dependency(
            entc.DependencyName("Writer"),
            entc.DependencyTypeInfo(&field.TypeInfo{
                Ident:   "io.Writer",
                PkgPath: "io",
            }),
        ),
    }
    if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
        log.Fatalf("ent-Codegenerierung fehlgeschlagen: %v", err)
    }
}

In diesem Beispiel fügen wir http.Client und io.Writer als Abhängigkeiten in die generierten Client-Objekte ein.

7. Ausgabe der Schema-Beschreibung

Im ent-Framework kann der Befehl ent describe verwendet werden, um die Beschreibung des Schemas in einem grafischen Format auszugeben. Dies kann Entwicklern dabei helfen, schnell die vorhandenen Entitäten und Beziehungen zu verstehen.

Führen Sie den folgenden Befehl aus, um die Beschreibung des Schemas zu erhalten:

go run -mod=mod entgo.io/ent/cmd/ent describe ./ent/schema

Der obige Befehl gibt eine Tabelle ähnlich der folgenden aus, die Informationen wie Felder, Typen, Beziehungen usw. für jede Entität anzeigt:

Benutzer:
    +-------+---------+--------+-----------+ ...
    | Feld  |  Typ    | Eindeutig | Optional  | ...
    +-------+---------+--------+-----------+ ...
    | id    | int     | false  | false     | ...
    | name  | string  | true   | false     | ...
    +-------+---------+--------+-----------+ ...
    +-------+--------+---------+-----------+ ...
    | Kante |  Typ   | Invers | Beziehung | ...
    +-------+--------+---------+-----------+ ...
    | pets  | Pet    | false   | O2M       | ...
    +-------+--------+---------+-----------+ ...

8. Code-Generierungshooks

8.1 Konzept der Hooks

Hooks sind Zwischenfunktions, die in den ent-Codegenerierungsprozess eingefügt werden können und es ermöglichen, benutzerdefinierte Logik vor und nach der Generierung des Codes einzufügen. Hooks können verwendet werden, um den abstrakten Syntaxbaum (AST) des generierten Codes zu manipulieren, Validierungen durchzuführen oder zusätzliche Code-Snippets hinzuzufügen.

8.2 Beispiel zur Verwendung von Hooks

Hier ist ein Beispiel zur Verwendung eines Hooks, um sicherzustellen, dass alle Felder ein bestimmtes Struktur-Tag enthalten (z.B. json):

func main() {
    err := entc.Generate("./schema", &gen.Config{
        Hooks: []gen.Hook{
            EnsureStructTag("json"),
        },
    })
    if err != nil {
        log.Fatalf("ent-Codegenerierung fehlgeschlagen: %v", err)
    }
}

// EnsureStructTag stellt sicher, dass alle Felder im Graphen ein bestimmtes Struktur-Tag enthalten
func EnsureStructTag(name string) gen.Hook {
    return func(next gen.Generator) gen.Generator {
        return gen.GenerateFunc(func(g *gen.Graph) error {
            for _, node := range g.Nodes {
                for _, field := range node.Fields {
                    tag := reflect.StructTag(field.StructTag)
                    if _, ok := tag.Lookup(name); !ok {
                        return fmt.Errorf("Struktur-Tag %q fehlt für das Feld %s.%s", name, node.Name, field.Name)
                    }
                }
            }
            return next.Generate(g)
        })
    }
}

In diesem Beispiel überprüft die Funktion EnsureStructTag vor der Codegenerierung jedes Feld auf das json-Tag. Wenn einem Feld dieses Tag fehlt, wird die Codegenerierung abgebrochen und ein Fehler zurückgegeben. Dies ist eine effektive Möglichkeit, um die Code-Sauberkeit und -Konsistenz zu gewährleisten.