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.