1. Podstawowe pojęcia dotyczące encji i skojarzeń

W frameworku ent, encja odnosi się do podstawowej jednostki danych zarządzanej w bazie danych, która zazwyczaj odpowiada tabeli w bazie danych. Pola w encji odpowiadają kolumnom w tabeli, podczas gdy skojarzenia (krawędzie) między encjami służą do opisywania relacji i zależności między encjami. Skojarzenia encji stanowią podstawę konstruowania złożonych modeli danych, pozwalając na reprezentację hierarchicznych relacji, takich jak relacje rodzic-dziecko i relacje właścicielskie.

Framework ent zapewnia bogaty zestaw interfejsów API, pozwalając programistom definiować i zarządzać tymi skojarzeniami w schemacie encji. Dzięki tym skojarzeniom możemy łatwo wyrażać i operować złożoną logiką biznesową między danymi.

2. Rodzaje skojarzeń encji w ent

2.1 Skojarzenie jeden do jeden (O2O)

Skojarzenie jeden do jeden odnosi się do jednoznacznej korespondencji między dwiema encjami. Na przykład, w przypadku użytkowników i kont bankowych, każdy użytkownik może mieć tylko jeden rachunek bankowy, a każdy rachunek bankowy również należy tylko do jednego użytkownika. Framework ent wykorzystuje metody edge.To i edge.From do zdefiniowania takich skojarzeń.

Najpierw możemy zdefiniować skojarzenie jeden do jeden wskazujące na Card w ramach schematu User:

// Krawędzie użytkownika.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("card", Card.Type). // Wskaże na encję Card, definiując nazwę skojarzenia jako "card"
            Unique(),               // Metoda Unique zapewnia, że jest to skojarzenie jeden do jeden
    }
}

Następnie definiujemy odwrotne skojarzenie z powrotem do User w ramach schematu Card:

// Krawędzie karty.
func (Card) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Wskazuje z powrotem do użytkownika z karty, definiując nazwę skojarzenia jako "owner"
            Ref("card").              // Metoda Ref określa odpowiadającą nazwę odwrotnego skojarzenia
            Unique(),                 // Oznaczone jako unikalne, aby zapewnić, że jedna karta odpowiada jednemu właścicielowi
    }
}

2.2 Skojarzenie jeden do wielu (O2M)

Skojarzenie jeden do wielu oznacza, że jedna encja może być skojarzona z kilkoma innymi encjami, ale te encje mogą wskazywać z powrotem tylko na jedną encję. Na przykład użytkownik może mieć wiele zwierząt domowych, ale każde zwierzę domowe ma tylko jednego właściciela.

W ent nadal używamy edge.To i edge.From do zdefiniowania tego rodzaju skojarzenia. Poniższy przykład definiuje skojarzenie jeden do wielu między użytkownikami a zwierzętami domowymi:

// Krawędzie użytkownika.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("pets", Pet.Type), // Skojarzenie jeden do wielu z encji User do encji Pet
    }
}

W encji Pet definiujemy wiele do jednego skojarzenia z powrotem do User:

// Krawędzie zwierzęcia domowego.
func (Pet) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Wiele do jednego skojarzenie z Pet do User
            Ref("pets").              // Określa nazwę odwrotnego skojarzenia z zwierzęcia domowego do właściciela
            Unique(),                 // Zapewnia, że jeden właściciel może mieć wiele zwierząt domowych
    }
}

2.3 Skojarzenie wiele do wielu (M2M)

Skojarzenie wiele do wielu pozwala, że dwa rodzaje encji mogą mieć wiele instancji siebie nawzajem. Na przykład student może zapisać się na wiele kursów, a kurs może także mieć wielu zapisanych studentów. ent udostępnia interfejs API do ustanowienia skojarzeń wiele do wielu:

W encji Student używamy edge.To, aby ustanowić skojarzenie wiele do wielu z encją Course:

// Krawędzie studenta.
func (Student) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("courses", Course.Type), // Definiuje skojarzenie wiele do wielu między Student a Course
    }
}

Podobnie, w encji Course ustanawiamy odwrotne skojarzenie do Student dla relacji wiele do wielu:

// Krawędzie kursu.
func (Course) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("students", Student.Type). // Definiuje skojarzenie wiele do wielu między Course a Student
            Ref("courses"),                  // Określa nazwę odwrotnego skojarzenia z Course do Student
    }
}

Te rodzaje skojarzeń stanowią fundament budowy złożonych modeli danych aplikacji, a zrozumienie sposobu ich definiowania i wykorzystywania w ent jest kluczowe dla rozbudowy modeli danych i logiki biznesowej.

3. Podstawowe operacje dla powiązań encji

Ten rozdział pokaże, jak wykonywać podstawowe operacje przy użyciu ent z zdefiniowanymi powiązaniami, w tym tworzenie, wyszukiwanie i przemieszczanie powiązanych encji.

3.1 Tworzenie powiązanych encji

Podczas tworzenia encji możesz jednocześnie ustawiać powiązania między nimi. W przypadku relacji jeden-do-wielu (O2M) i wiele-do-wielu (M2M) możesz użyć metody Add{Edge} do dodawania powiązanych encji.

Na przykład, jeśli mamy encję użytkownika i encję zwierzęcia domowego z określonym powiązaniem, gdzie użytkownik może mieć wiele zwierząt domowych, poniżej znajduje się przykład tworzenia nowego użytkownika i dodawania dla niego zwierząt:

// Stwórz użytkownika i dodaj zwierzęta
func CreateUserWithPets(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Utwórz egzemplarz zwierzęcia domowego
    fido := client.Pet.
        Create().  
        SetName("Fido").
        SaveX(ctx)
    // Utwórz egzemplarz użytkownika i połącz go ze zwierzęciem domowym
    user := client.User.
        Create().
        SetName("Alice").
        AddPets(fido). // Użyj metody AddPets do połączenia zwierzęcia domowego
        SaveX(ctx)

    return user, nil
}

W tym przykładzie najpierw tworzymy egzemplarz zwierzęcia domowego o nazwie Fido, a następnie tworzymy użytkownika o nazwie Alice i powiązujemy egzemplarz zwierzęcia domowego z użytkownikiem przy użyciu metody AddPets.

3.2 Wyszukiwanie powiązanych encji

Wyszukiwanie powiązanych encji jest powszechną operacją w ent. Na przykład, możesz użyć metody Query{Edge} do pobrania innych encji powiązanych z określoną encją.

Kontynuując nasz przykład z użytkownikami i zwierzętami, oto jak wyszukać wszystkie zwierzęta należące do danego użytkownika:

// Zapytaj o wszystkie zwierzęta użytkownika
func QueryUserPets(ctx context.Context, client *ent.Client, userID int) ([]*ent.Pet, error) {
    pets, err := client.User.
        Get(ctx, userID). // Pobierz instancję użytkownika na podstawie identyfikatora użytkownika
        QueryPets().      // Zapytaj encje zwierząt powiązane z użytkownikiem
        All(ctx)          // Zwróć wszystkie wyszukane encje zwierząt
    if err != nil {
        return nil, err
    }

    return pets, nil
}

W powyższym fragmencie kodu najpierw pobieramy instancję użytkownika na podstawie identyfikatora użytkownika, a następnie wywołujemy metodę QueryPets, aby pobrać wszystkie encje zwierząt powiązanych z tym użytkownikiem.

Uwaga: Narzędzie generowania kodu ent automatycznie generuje interfejs API dla zapytań o powiązania na podstawie zdefiniowanych relacji między encjami. Zaleca się przejrzenie wygenerowanego kodu.

4. Skrośne ładowanie

4.1 Zasady wstępnego ładowania

Wstępne ładowanie to technika używana podczas zapytań do baz danych w celu wcześniejszego pobrania i ładowania powiązanych encji. Ta metoda jest powszechnie stosowana do pozyskiwania danych związanych z wieloma encjami na raz, aby uniknąć wielu operacji zapytań do bazy danych w późniejszym przetwarzaniu, co znacząco poprawia wydajność aplikacji.

W frameworku ent, wstępne ładowanie służy głównie do obsługi powiązań między encjami, takich jak jeden-do-wielu i wiele-do-wielu. Podczas pobierania encji z bazy danych, ich powiązane encje nie są automatycznie ładowane. Zamiast tego są one ładowane wymuszenie poprzez wstępne ładowanie. Jest to istotne dla rozwiązania problemu zapytań typu N+1 (tj. wykonywanie osobnych zapytań dla każdej encji nadrzędnej).

W frameworku ent, wstępne ładowanie jest osiągane poprzez użycie metody With w generatorze zapytań. Ta metoda generuje odpowiadające funkcje With... dla każdego powiązania, takie jak WithGrupy i WithZwierzaki. Te metody są automatycznie generowane przez framework ent, a programiści mogą ich używać do żądania wstępnego ładowania określonych powiązań.

Zasada działania ładowania wstępnego encji polega na tym, że podczas zapytania o główną encję, ent wykonuje dodatkowe zapytania, aby pobrać wszystkie powiązane encje. Następnie te encje są uzupełniane w polu Edges zwróconego obiektu. Oznacza to, że ent może wykonywać wiele zapytań do bazy danych, przynajmniej raz dla każdego powiązanego krawędzi, której wstępne ładowanie jest wymagane. Chociaż ta metoda może być mniej efektywna niż pojedyncze złożone zapytanie JOIN w niektórych scenariuszach, oferuje większą elastyczność i jest przewidywane, że otrzyma optymalizacje wydajności w przyszłych wersjach frameworka ent.

4.2 Implementacja wstępnego ładowania

Teraz pokażemy, jak wykonywać operacje wstępnego ładowania w frameworku ent za pomocą przykładowego kodu, korzystając z modeli użytkowników i zwierząt domowych omówionych we wstępie.

Wstępne ładowanie pojedynczego powiązania

Załóżmy, że chcemy pobrać wszystkich użytkowników z bazy danych i wstępnie załadować dane dotyczące zwierząt domowych. Możemy to osiągnąć, pisząc następujący kod:

users, err := client.User.
    Query().
    WithPets().
    All(ctx)
if err != nil {
    // Obsłuż błąd
    return err
}
for _, u := range users {
    for _, p := range u.Edges.Pets {
        fmt.Printf("Użytkownik (%v) posiada zwierzę domowe (%v)\n", u.ID, p.ID)
    }
}

W tym przykładzie używamy metody WithPets, aby poprosić ent o wstępne załadowanie powiązanych z użytkownikami zwierząt domowych. Wstępnie załadowane dane dotyczące zwierząt domowych są umieszczone w polu Edges.Pets, z którego możemy uzyskać dostęp do tych powiązanych danych.

Wstępne ładowanie wielu powiązań

ent pozwala nam na wstępne ładowanie wielu powiązań naraz, a nawet określanie wstępnego ładowania zagnieżdżonych powiązań, filtrowania, sortowania lub ograniczania liczby wstępnie załadowanych wyników. Poniżej znajduje się przykład wstępnego ładowania zwierząt domowych administratorów oraz zespołów, do których należą, jednocześnie wstępnie ładowanych są także użytkownicy zespołów:

admins, err := client.User.
    Query().
    Where(user.Admin(true)).
    WithPets().
    WithGroups(func(q *ent.GroupQuery) {
        q.Limit(5)          // Ograniczenie do pierwszych 5 zespołów
        q.Order(ent.Asc(group.FieldName)) // Sortowanie rosnąco według nazwy zespołu
        q.WithUsers()       // Wstępne załadowanie użytkowników w zespole
    }).
    All(ctx)
if err != nil {
    // Obsłuż błędy
    return err
}
for _, admin := range admins {
    for _, p := range admin.Edges.Pets {
        fmt.Printf("Administrator (%v) posiada zwierzę domowe (%v)\n", admin.ID, p.ID)
    }
    for _, g := range admin.Edges.Groups {
        fmt.Printf("Administrator (%v) należy do zespołu (%v)\n", admin.ID, g.ID)
        for _, u := range g.Edges.Users {
            fmt.Printf("Zespół (%v) ma członka (%v)\n", g.ID, u.ID)
        }
    }
}

Poprzez ten przykład możesz zobaczyć, jak potężny i elastyczny jest ent. Zaledwie kilkoma prostymi wywołaniami metod może wstępnie załadować bogate dane powiązane i zorganizować je w uporządkowany sposób. Zapewnia to dużą wygodę podczas tworzenia aplikacji opartych na danych.