1. Podstawy Modelu i Pola

1.1. Wprowadzenie do Definicji Modelu

W ramach frameworka ORM model służy do opisywania relacji mapowania między typami encji w aplikacji a tabelami w bazie danych. Model definiuje właściwości i relacje encji, a także powiązane z nimi konfiguracje specyficzne dla bazy danych. W frameworku ent modele są zwykle używane do opisywania typów encji w grafie, takich jak User lub Group.

Definicje modeli zazwyczaj obejmują opisy pól encji (lub właściwości) i krawędzi (lub relacji), a także specyficzne dla bazy danych opcje. Te opisy mogą pomóc nam zdefiniować strukturę, właściwości i relacje encji, oraz mogą być używane do generowania odpowiadającej struktury tabeli w bazie danych na podstawie modelu.

1.2. Przegląd Pola

Pola stanowią część modelu reprezentującą właściwości encji. Definiują one właściwości encji, takie jak imię, wiek, data, itp. W frameworku ent typy pól obejmują różne podstawowe typy danych, takie jak całkowity, łańcuch znaków, logiczny, czasowy, itp., a także niektóre specyficzne dla SQL, takie jak UUID, []byte, JSON, itp.

Poniższa tabela przedstawia typy pól obsługiwane przez framework ent:

Typ Opis
int Typ całkowity
uint8 Bez znaku 8-bitowy typ całkowity
float64 Typ zmiennoprzecinkowy
bool Typ logiczny
string Typ łańcuchowy
time.Time Typ czasowy
UUID Typ UUID
[]byte Typ tablicy bajtów (tylko SQL)
JSON Typ JSON (tylko SQL)
Enum Typ Enum (tylko SQL)
Inny Inne typy (np. zakres Postgres)

2. Szczegóły Właściwości Pola

2.1. Typy Danych

Typ danych atrybutu lub pola w modelu encji określa formę danych, które można przechowywać. Jest to istotna część definicji modelu w frameworku ent. Poniżej przedstawiono kilka powszechnie używanych typów danych w frameworku ent.

import (
    "time"

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

// Schemat użytkownika.
type User struct {
    ent.Schema
}

// Pola użytkownika.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age"),             // Typ całkowity
        field.String("name"),         // Typ łańcuchowy
        field.Bool("active"),         // Typ logiczny
        field.Float("score"),         // Typ zmiennoprzecinkowy
        field.Time("created_at"),     // Typ znacznika czasu
    }
}
  • int: Reprezentuje wartości całkowite, które mogą być int8, int16, int32, int64, itp.
  • string: Reprezentuje dane tekstowe.
  • bool: Reprezentuje wartości logiczne, zazwyczaj używane jako flagi.
  • float64: Reprezentuje liczby zmiennoprzecinkowe, można również używać float32.
  • time.Time: Reprezentuje czas, zazwyczaj używany do znaczników czasu lub danych daty.

Te typy pól zostaną zmapowane na odpowiadające typy obsługiwane przez bazę danych. Ponadto ent obsługuje bardziej skomplikowane typy takie jak UUID, JSON, enumy (Enum), oraz obsługę specjalnych typów baz danych jak []byte (tylko SQL) i Other (tylko SQL).

2.2. Wartości Domyślne

Pola mogą być skonfigurowane z wartościami domyślnymi. Jeśli odpowiadająca wartość nie jest określona podczas tworzenia encji, będzie używana wartość domyślna. Wartość domyślna może być stałą wartością lub dynamicznie generowaną wartością z funkcji. Użyj metody .Default aby ustawić statyczną wartość domyślną, lub użyj .DefaultFunc aby ustawić dynamicznie generowaną wartość domyślną.

// Schemat użytkownika.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Time("created_at").
            Default(time.Now),  // Stała domyślna wartość time.Now
        field.String("role").
            Default("user"),   // Stała wartość łańcuchowa
        field.Float("score").
            DefaultFunc(func() float64 {
                return 10.0  // Wartość domyślna wygenerowana przez funkcję
            }),
    }
}

2.3. Opcjonalność pola i wartości zerowe

Domyślnie pola są wymagane. Aby zadeklarować pole opcjonalne, użyj metody .Optional(). Opcjonalne pola zostaną zadeklarowane jako pola anulowalne w bazie danych. Opcja Nillable pozwala na jawne ustawienie pól na nil, rozróżniając między wartością zerową pola a stanem nieustawionym.

// Schema użytkownika.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("nickname").Optional(), // Pole opcjonalne nie jest wymagane
        field.Int("wiek").Optional().Nillable(), // Pole nillable może być ustawione na nil
    }
}

Korzystając z powyższego zdefiniowanego modelu, pole wiek może przyjmować zarówno wartości nil, aby wskazać nieustawiony stan, jak i niezerowe wartości nil.

2.4. Unikalność pola

Unikalne pola zapewniają, że nie ma zduplikowanych wartości w tabeli bazy danych. Użyj metody Unique(), aby zdefiniować pole unikalne. Gdy zapewnienie integralności danych jest wymaganiem krytycznym, np. dla adresów e-mail użytkowników lub nazw użytkowników, należy używać pól unikalnych.

// Schema użytkownika.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("email").Unique(),  // Pole unikalne, aby uniknąć zduplikowanych adresów e-mail
    }
}

Spowoduje to utworzenie ograniczenia unikalności w bazie danych, aby zapobiec wstawianiu zduplikowanych wartości.

2.5. Indeksowanie pola

Indeksowanie pola służy do poprawy wydajności zapytań do bazy danych, szczególnie w dużych bazach danych. W frameworku ent, metodę .Indexes() można użyć do tworzenia indeksów.

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

// Schema użytkownika.
func (User) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("email"),  // Utwórz indeks na polu 'email'
        index.Fields("nazwa", "wiek").Unique(), // Utwórz unikalny indeks złożony
    }
}

Indeksy mogą być wykorzystywane dla pól często przeszukiwanych, ale ważne jest zauważenie, że zbyt wiele indeksów może prowadzić do obniżenia wydajności operacji zapisu. Dlatego decyzja o tworzeniu indeksów powinna być zrównoważona na podstawie rzeczywistych okoliczności.

2.6. Tagi niestandardowe

W frameworku ent można użyć metody StructTag do dodawania niestandardowych tagów do wygenerowanych pól struktury obiektu. Tagi te są bardzo przydatne do implementacji operacji takich jak kodowanie JSON i kodowanie XML. W poniższym przykładzie dodamy niestandardowe tagi JSON i XML dla pola nazwa.

// Pola użytkownika.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("nazwa").
            // Dodaj niestandardowe tagi za pomocą metody StructTag
            // Tutaj ustaw tag JSON dla pola _name_ na 'nazwa_użytkownika' i zignoruj go, gdy pole jest puste (omitempty)
            // Ustaw także tag XML do kodowania na 'nazwa'
            StructTag(`json:"nazwa_użytkownika,omitempty" xml:"nazwa"`),
    }
}

Podczas kodowania JSON lub XML opcja omitempty oznacza, że jeśli pole nazwa jest puste, to pole to zostanie pominięte z wyniku kodowania. Jest to bardzo przydatne do zmniejszenia rozmiaru ciała odpowiedzi podczas pisania interfejsów API.

Pokazano również, jak ustawić wiele tagów dla tego samego pola jednocześnie. Tagi JSON używają klucza json, tagi XML używają klucza xml, i są one oddzielone spacjami. Te tagi zostaną użyte przez funkcje biblioteczne takie jak encoding/json i encoding/xml podczas analizy struktury do kodowania lub dekodowania.

3. Walidacja i ograniczenia pola

Walidacja pola to ważny aspekt projektowania bazy danych, mający na celu zapewnienie spójności i poprawności danych. W tej sekcji omówimy wykorzystanie wbudowanych walidatorów, niestandardowych walidatorów, oraz różnych ograniczeń w celu poprawy integralności i jakości danych w modelu encji.

3.1. Wbudowani walidatorzy

Framework dostarcza szereg wbudowanych walidatorów do wykonywania podstawowych sprawdzeń poprawności danych na różnych rodzajach pól. Użycie tych wbudowanych walidatorów może uproszczać proces tworzenia i szybko definiować zakresy lub formaty poprawnych danych dla pól.

Oto kilka przykładów wbudowanych walidatorów pól:

  • Walidatory dla typów liczbowych:
    • Positive(): Sprawdza, czy wartość pola jest liczbą dodatnią.
    • Negative(): Sprawdza, czy wartość pola jest liczbą ujemną.
    • NonNegative(): Sprawdza, czy wartość pola jest liczbą nieujemną.
    • Min(i): Sprawdza, czy wartość pola jest większa niż określona minimalna wartość i.
    • Max(i): Sprawdza, czy wartość pola jest mniejsza niż określona maksymalna wartość i.
  • Walidatory dla typu string:
    • MinLen(i): Sprawdza minimalną długość ciągu znaków.
    • MaxLen(i): Sprawdza maksymalną długość ciągu znaków.
    • Match(regexp.Regexp): Sprawdza, czy ciąg znaków pasuje do określonego wyrażenia regularnego.
    • NotEmpty: Sprawdza, czy ciąg znaków nie jest pusty.

Spójrzmy na praktyczny przykład kodu. W tym przykładzie tworzony jest model User, który zawiera pole typu nieujemnej liczby całkowitej age oraz pole email o określonym formacie:

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. Niestandardowi walidatorzy

Chociaż wbudowani walidatorzy mogą obsługiwać wiele powszechnych wymagań walidacji, czasami możesz potrzebować bardziej skomplikowanej logiki walidacji. W takich przypadkach można pisać niestandardowe walidatory, aby spełnić określone reguły biznesowe.

Niestandardowy walidator to funkcja, która przyjmuje wartość pola i zwraca error. Jeśli zwrócony error nie jest pusty, oznacza to niepowodzenie walidacji. Ogólny format niestandardowego walidatora jest następujący:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("phone").
            Validate(func(s string) error {
                // Sprawdź, czy numer telefonu spełnia oczekiwany format
                dopasowany, _ := regexp.MatchString(`^\+?[1-9]\d{1,14}$`, s)
                if !dopasowany {
                    return errors.New("Nieprawidłowy format numeru telefonu")
                }
                return nil
            }),
    }
}

Jak pokazano powyżej, stworzyliśmy niestandardowy walidator do sprawdzania formatu numeru telefonu.

3.3. Ograniczenia

Ograniczenia to reguły narzucane na obiekt bazy danych. Mogą służyć do zapewnienia poprawności i spójności danych, takich jak zapobieganie wprowadzeniu nieprawidłowych danych lub definiowanie relacji między danymi.

Powszechne ograniczenia bazy danych obejmują:

  • Ograniczenie klucza głównego: Zapewnia, że każdy rekord w tabeli jest unikalny.
  • Ograniczenie unikalności: Zapewnia, że wartość kolumny lub kombinacji kolumn jest unikalna w tabeli.
  • Ograniczenie klucza obcego: Definiuje relacje między tabelami, zapewniając integralność referencyjną.
  • Ograniczenie sprawdzania: Zapewnia, że wartość pola spełnia określony warunek.

W modelu jednostki możesz definiować ograniczenia w celu utrzymania integralności danych, jak pokazano poniżej:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("username").
            Unique(), // Ograniczenie unikalności, aby upewnić się, że nazwa użytkownika jest unikalna w tabeli.
        field.String("email").
            Unique(), // Ograniczenie unikalności, aby upewnić się, że adres e-mail jest unikalny w tabeli.
    }
}

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("friends", User.Type).
            Unique(), // Ograniczenie klucza obcego, tworzące unikalny związek krawędzi z innym użytkownikiem.
    }
}

Podsumowując, walidacja pola i ograniczenia są kluczowe dla zapewnienia dobrej jakości danych i unikania nieoczekiwanych błędów danych. Wykorzystanie narzędzi dostarczanych przez framework ent może ułatwić i bardziej niezawodnym proces ten proces.