1. Model- und Feld-Grundlagen

1.1. Einführung in die Modelldefinition

In einem ORM-Framework wird ein Modell verwendet, um die Zuordnungsbeziehung zwischen Entitätstypen in der Anwendung und Datenbanktabellen zu beschreiben. Das Modell definiert die Eigenschaften und Beziehungen der Entität sowie die datenbankspezifischen Konfigurationen, die mit ihnen verbunden sind. Im ent-Framework werden Modelle typischerweise verwendet, um Entitätstypen in einem Graphen zu beschreiben, wie z.B. Benutzer oder Gruppe.

Modelldefinitionen umfassen in der Regel Beschreibungen der Felder (oder Eigenschaften) und Kanten (oder Beziehungen) der Entität sowie einige datenbankspezifische Optionen. Diese Beschreibungen können uns dabei helfen, die Struktur, Eigenschaften und Beziehungen der Entität zu definieren und auf Grundlage des Modells die entsprechende Tabellenstruktur in der Datenbank zu generieren.

1.2. Feldübersicht

Felder repräsentieren im Modell den Teil, der die Eigenschaften der Entität darstellt. Sie definieren die Eigenschaften der Entität, wie z.B. Name, Alter, Datum usw. Im ent-Framework umfassen die Feldtypen verschiedene grundlegende Datentypen wie Integer, String, Boolean, Zeit usw. sowie einige SQL-spezifische Typen wie UUID, []byte, JSON usw.

Die folgende Tabelle zeigt die von ent unterstützten Feldtypen:

Typ Beschreibung
int Ganzzahliger Typ
uint8 Vorzeichenloser 8-Bit-Ganzzahlentyp
float64 Typ für Gleitkommazahlen
bool Boolescher Typ
string Zeichentyp
time.Time Zeitstempeltyp
UUID UUID-Typ
[]byte Byte-Array-Typ (nur SQL)
JSON JSON-Typ (nur SQL)
Enum Enum-Typ (nur SQL)
Other Andere Typen (z. B. Postgres Range)

2. Details zur Feldattribute

2.1. Datentypen

Der Datentyp eines Attributs oder Feldes in einem Entitätsmodell bestimmt die Form der zu speichernden Daten. Dies ist ein entscheidender Bestandteil der Modelldefinition im ent-Framework. Hier sind einige häufig verwendete Datentypen im ent-Framework.

import (
    "time"
 
    "entgo.io/ent"
    "entgo.io/ent/schema/field"
)
 
// Benutzerschema.
type Benutzer struct {
    ent.Schema
}
 
// Felder des Benutzers.
func (Benutzer) Fields() []ent.Field {
    return []ent.Field{
        field.Int("alter"),             // Integer-Typ
        field.String("name"),         // String-Typ
        field.Bool("aktiv"),         // Boolescher Typ
        field.Float("punktzahl"),         // Gleitkommazahl-Typ
        field.Time("erstellt_am"),     // Zeitstempel-Typ
    }
}
  • int: Stellt Ganzzahlwerte dar, die int8, int16, int32, int64 usw. sein können.
  • string: Repräsentiert Zeichendaten.
  • bool: Repräsentiert boolesche Werte, die typischerweise als Flags verwendet werden.
  • float64: Repräsentiert Gleitkommazahlen, auch float32 kann verwendet werden.
  • time.Time: Repräsentiert Zeit und wird typischerweise für Zeitstempel oder Datumswerte verwendet.

Diese Feldtypen werden auf die entsprechenden Typen abgebildet, die von der zugrunde liegenden Datenbank unterstützt werden. Darüber hinaus unterstützt ent komplexere Typen wie UUID, JSON, Enums (Enum) und die Unterstützung für spezielle Datenbanktypen wie []byte (nur SQL) und Other (nur SQL).

2.2. Standardwerte

Felder können mit Standardwerten konfiguriert werden. Wenn der entsprechende Wert beim Erstellen einer Entität nicht angegeben wird, wird der Standardwert verwendet. Der Standardwert kann ein fester Wert oder ein dynamisch generierter Wert aus einer Funktion sein. Verwenden Sie die Methode .Default, um einen statischen Standardwert festzulegen, oder verwenden Sie .DefaultFunc, um einen dynamisch generierten Standardwert festzulegen.

// Benutzerschema.
func (Benutzer) Fields() []ent.Field {
    return []ent.Field{
        field.Time("erstellt_am").
            Default(time.Now),  // Ein fester Standardwert von time.Now
        field.String("rolle").
            Default("benutzer"),   // Ein konstanter Zeichenkettenwert
        field.Float("punktzahl").
            DefaultFunc(func() float64 {
                return 10.0  // Ein durch eine Funktion generierter Standardwert
            }),
    }
}

2.3. Feldoptionalität und Nullwerte

Standardmäßig sind Felder erforderlich. Verwenden Sie die Methode .Optional(), um ein optionales Feld zu deklarieren. Optionale Felder werden als nullable Felder in der Datenbank deklariert. Die Option Nillable ermöglicht es, Felder explizit auf nil zu setzen und somit zwischen dem Nullwert eines Feldes und einem nicht gesetzten Zustand zu unterscheiden.

// Benutzer-Schema.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("nickname").Optional(), // Optionales Feld ist nicht erforderlich
        field.Int("age").Optional().Nillable(), // Nillable-Feld kann als nil gesetzt werden
    }
}

Bei Verwendung des oben definierten Modells kann das Feld age sowohl nil-Werte akzeptieren, um einen nicht gesetzten Zustand anzuzeigen, als auch Nicht-nil-Nullwerte.

2.4. Feldeinmaligkeit

Einmalige Felder stellen sicher, dass es keine doppelten Werte in der Datenbanktabelle gibt. Verwenden Sie die Methode Unique(), um ein einmaliges Feld zu definieren. Wenn die Datintegrität eine wichtige Anforderung ist, beispielsweise für Benutzer-E-Mails oder Benutzernamen, sollten einmalige Felder verwendet werden.

// Benutzer-Schema.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("email").Unique(),  // Einmaliges Feld, um doppelte E-Mail-Adressen zu vermeiden
    }
}

Dadurch wird eine eindeutige Einschränkung in der zugrunde liegenden Datenbank erstellt, um das Einfügen von doppelten Werten zu verhindern.

2.5. Feldindizierung

Die Feldindizierung dient dazu, die Leistung von Datenbankabfragen zu verbessern, insbesondere in großen Datenbanken. Im ent-Framework kann die Methode .Indexes() verwendet werden, um Indizes zu erstellen.

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

// Benutzer-Schema.
func (User) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("email"),  // Erstellt einen Index auf dem Feld 'email'
        index.Fields("name", "age").Unique(), // Erstellt einen eindeutigen zusammengesetzten Index
    }
}

Indizes können für häufig abgefragte Felder verwendet werden, jedoch ist zu beachten, dass zu viele Indizes zu einer verminderten Schreibleistung führen können. Daher sollte die Entscheidung zur Erstellung von Indizes basierend auf den tatsächlichen Umständen ausbalanciert werden.

2.6. Benutzerdefinierte Tags

Im ent-Framework können Sie die Methode StructTag verwenden, um benutzerdefinierte Tags zu den generierten Entity-Strukturfeldern hinzuzufügen. Diese Tags sind sehr nützlich für die Implementierung von Operationen wie der JSON-Codierung und der XML-Codierung. Im folgenden Beispiel fügen wir benutzerdefinierte JSON- und XML-Tags für das name-Feld hinzu.

// Felder des Benutzers.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            // Fügen Sie benutzerdefinierte Tags mit der Methode StructTag hinzu
            // Setzen Sie hier das JSON-Tag für das Name-Feld auf 'Benutzername' und ignorieren Sie es, wenn das Feld leer ist (omitempty)
            // Setzen Sie auch das XML-Tag für die Codierung auf 'name'
            StructTag(`json:"username,omitempty" xml:"name"`),
    }
}

Beim Codieren mit JSON oder XML gibt die Option omitempty an, dass das Feld name aus dem Codierungsergebnis weggelassen wird, wenn es leer ist. Dies ist sehr nützlich, um die Größe des Antwortkörpers beim Schreiben von APIs zu reduzieren.

Dies zeigt auch, wie mehrere Tags für dasselbe Feld gleichzeitig gesetzt werden. JSON-Tags verwenden den Schlüssel json, XML-Tags verwenden den Schlüssel xml, und sie werden durch Leerzeichen getrennt. Diese Tags werden von Bibliotheksfunktionen wie encoding/json und encoding/xml beim Analysieren von Strukturen für die Codierung oder Decodierung verwendet.

3. Feldvalidierung und Einschränkungen

Die Feldvalidierung ist ein wichtiger Aspekt des Datenbankentwurfs, um die Datenkonsistenz und -gültigkeit sicherzustellen. In diesem Abschnitt werden wir die Verwendung von integrierten Validatoren, benutzerdefinierten Validatoren und verschiedenen Einschränkungen untersuchen, um die Integrität und Qualität der Daten im Entitätsmodell zu verbessern.

3.1. Eingebaute Validatoren

Das Framework bietet eine Reihe von eingebauten Validatoren, um häufige Datenvaliditätsprüfungen für verschiedene Arten von Feldern durchzuführen. Die Verwendung dieser eingebauten Validatoren kann den Entwicklungsprozess vereinfachen und schnell gültige Datenbereiche oder -formate für Felder definieren.

Hier sind einige Beispiele für eingebaute Feldvalidatoren:

  • Validatoren für numerische Typen:
    • Positive(): Prüft, ob der Wert des Feldes eine positive Zahl ist.
    • Negative(): Prüft, ob der Wert des Feldes eine negative Zahl ist.
    • NonNegative(): Prüft, ob der Wert des Feldes eine nicht-negative Zahl ist.
    • Min(i): Prüft, ob der Wert des Feldes größer als ein gegebener Minimalwert i ist.
    • Max(i): Prüft, ob der Wert des Feldes kleiner als ein gegebener Maximalwert i ist.
  • Validatoren für den Typ string:
    • MinLen(i): Prüft die minimale Länge eines Strings.
    • MaxLen(i): Prüft die maximale Länge eines Strings.
    • Match(regexp.Regexp): Prüft, ob der String mit dem angegebenen regulären Ausdruck übereinstimmt.
    • NotEmpty: Prüft, ob der String nicht leer ist.

Werfen wir einen Blick auf ein praktisches Code-Beispiel. In diesem Beispiel wird ein User-Modell erstellt, das ein nicht-negatives Integer-Typ-Feld age und ein email-Feld mit einem festen Format enthält:

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. Benutzerdefinierte Validatoren

Während eingebaute Validatoren viele gängige Validierungsanforderungen erfüllen können, benötigen Sie manchmal komplexere Validierungslogik. In solchen Fällen können Sie benutzerdefinierte Validatoren schreiben, um spezifische Geschäftsregeln zu erfüllen.

Ein benutzerdefinierter Validator ist eine Funktion, die einen Feldwert empfängt und einen error zurückgibt. Wenn der zurückgegebene error nicht leer ist, deutet dies auf eine Validierungsfehler hin. Das allgemeine Format eines benutzerdefinierten Validierers ist wie folgt:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("phone").
            Validate(func(s string) error {
                // Überprüfen, ob die Telefonnummer das erwartete Format erfüllt
                matched, _ := regexp.MatchString(`^\+?[1-9]\d{1,14}$`, s)
                if !matched {
                    return errors.New("Falsches Telefonnummerformat")
                }
                return nil
            }),
    }
}

Wie oben gezeigt, haben wir einen benutzerdefinierten Validator erstellt, um das Format einer Telefonnummer zu validieren.

3.3. Einschränkungen

Einschränkungen sind Regeln, die spezifische Regeln für ein Datenbankobjekt durchsetzen. Sie können verwendet werden, um die Richtigkeit und Konsistenz von Daten zu gewährleisten, z. B. um die Eingabe ungültiger Daten zu verhindern oder die Beziehungen von Daten zu definieren.

Gängige Datenbankeinschränkungen umfassen:

  • Primärschlüsseleinschränkung: Stellt sicher, dass jeder Datensatz in der Tabelle eindeutig ist.
  • Eindeutigkeitseinschränkung: Stellt sicher, dass der Wert einer Spalte oder einer Kombination von Spalten in der Tabelle eindeutig ist.
  • Fremdschlüsseleinschränkung: Definiert die Beziehungen zwischen Tabellen und stellt die referenzielle Integrität sicher.
  • Check-Einschränkung: Stellt sicher, dass ein Feldwert eine bestimmte Bedingung erfüllt.

Im Entitätsmodell können Einschränkungen definiert werden, um die Datenintegrität wie folgt zu erhalten:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("username").
            Unique(), // Eindeutigkeitseinschränkung, um sicherzustellen, dass der Benutzername in der Tabelle eindeutig ist.
        field.String("email").
            Unique(), // Eindeutigkeitseinschränkung, um sicherzustellen, dass die E-Mail in der Tabelle eindeutig ist.
    }
}

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("friends", User.Type).
            Unique(), // Fremdschlüsseleinschränkung, um eine eindeutige Kantenbeziehung zu einem anderen Benutzer zu erstellen.
    }
}

Zusammenfassend sind Feldvalidierung und Einschränkungen entscheidend, um die Datenqualität zu gewährleisten und unerwartete Datenfehler zu vermeiden. Die Nutzung der von der ent-Framework bereitgestellten Tools kann diesen Prozess einfacher und zuverlässiger machen.