1. Wprowadzenie do ent

Ent to framework encji opracowany przez Facebook specjalnie dla języka Go. Umożliwia uproszczenie procesu tworzenia i utrzymywania aplikacji modelu danych o dużej skali. Framework ent głównie stosuje się do następujących zasad:

  • Łatwo modelować schemat bazy danych jako strukturę grafu.
  • Definiować schemat w postaci kodu języka Go.
  • Wdrażać typy statyczne na podstawie generacji kodu.
  • Pisanie zapytań do bazy danych i poruszanie się po grafie jest bardzo proste.
  • Łatwe rozszerzanie i dostosowywanie za pomocą szablonów Go.

2. Konfiguracja środowiska

Aby zacząć korzystać z frameworka ent, upewnij się, że język Go jest zainstalowany w Twoim środowisku deweloperskim.

Jeśli katalog projektu znajduje się poza GOPATH, lub jeśli nie jesteś zaznajomiony z GOPATH, możesz użyć poniższej komendy, aby utworzyć nowy projekt modułowy Go:

go mod init entdemo

Spowoduje to zainicjowanie nowego modułu Go i utworzenie nowego pliku go.mod dla Twojego projektu entdemo.

3. Definiowanie Pierwszego Schematu

3.1. Tworzenie schematu za pomocą narzędzia ent CLI

Najpierw należy uruchomić poniższą komendę w katalogu głównym projektu, aby utworzyć schemat o nazwie User za pomocą narzędzia ent CLI:

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

Powyższa komenda wygeneruje schemat użytkownika w katalogu entdemo/ent/schema/:

Plik entdemo/ent/schema/user.go:

package schema

import "entgo.io/ent"

// User przechowuje definicję schematu dla encji Użytkownik.
type User struct {
    ent.Schema
}

// Pola Użytkownika.
func (User) Fields() []ent.Field {
    return nil
}

// Krawędzie Użytkownika.
func (User) Edges() []ent.Edge {
    return nil
}

3.2. Dodawanie pól

Następnie trzeba dodać definicje pól do schematu Użytkownika. Oto przykład dodania dwóch pól do encji Użytkownik.

Zmodyfikowany plik entdemo/ent/schema/user.go:

package schema

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

// Pola Użytkownika.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("name").
            Default("unknown"),
    }
}

Ten fragment kodu definiuje dwa pola dla modelu Użytkownika: age oraz name, gdzie age jest liczbą całkowitą dodatnią, a name jest ciągiem znaków o wartości domyślnej "unknown".

3.3. Generowanie Encji bazy danych

Po zdefiniowaniu schematu, należy uruchomić komendę go generate, aby wygenerować związane z nim logikę dostępu do bazy danych.

Uruchom poniższą komendę w katalogu głównym projektu:

go generate ./ent

Ta komenda wygeneruje odpowiadający kod Go na podstawie wcześniej zdefiniowanego schematu, powodując utworzenie następującej struktury plików:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... (kilka plików pominiętych dla zwięzłości)
├── schema
│   └── user.go
├── tx.go
├── user
│   ├── user.go
│   └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go

4.1. Inicjowanie połączenia z bazą danych

Aby nawiązać połączenie z bazą danych MySQL, możemy użyć funkcji Open udostępnionej przez framework ent. Najpierw zaimportuj sterownik MySQL, a następnie podaj prawidłowy ciąg połączenia, aby zainicjować połączenie z bazą danych.

package main

import (
    "context"
    "log"

    "entdemo/ent"
    
    _ "github.com/go-sql-driver/mysql" // Importuj sterownik MySQL
)

func main() {
    // Użyj ent.Open, aby nawiązać połączenie z bazą danych MySQL.
    // Pamiętaj, aby zamienić zastępcze wartości "your_username", "your_password" i "your_database" poniżej.
    client, err := ent.Open("mysql", "your_username:your_password@tcp(localhost:3306)/your_database?parseTime=True")
    if err != nil {
        log.Fatalf("nie udało się otworzyć połączenia z MySQL: %v", err)
    }
    defer client.Close()

    // Uruchom narzędzie do automatycznej migracji
    ctx := context.Background()
    if err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("nie udało się utworzyć zasobów schematu: %v", err)
    }
    
    // Tutaj można napisać dodatkową logikę biznesową
}

4.2. Tworzenie Encji

Tworzenie encji User polega na budowaniu nowego obiektu encji i zapisywaniu go do bazy danych za pomocą metody Save lub SaveX. Poniższy kod przedstawia, jak utworzyć nową encję User i zainicjować dwa pola age i name.

// Funkcja CreateUser służy do tworzenia nowej encji User
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Użyj client.User.Create(), aby zbudować żądanie utworzenia User,
    // następnie łącz metody SetAge oraz SetName, aby ustawić wartości pól encji.
    u, err := client.User.
        Create().
        SetAge(30).    // Ustaw wiek użytkownika
        SetName("a8m"). // Ustaw nazwę użytkownika
        Save(ctx)     // Wywołaj Save, aby zapisać encję w bazie danych
    if err != nil {
        return nil, fmt.Errorf("nie udało się utworzyć użytkownika: %w", err)
    }
    log.Println("użytkownik został utworzony: ", u)
    return u, nil
}

W funkcji main możesz wywołać funkcję CreateUser, aby utworzyć nową encję użytkownika.

func main() {
    // ... Pominięto kod dotyczący ustanowienia połączenia z bazą danych

    // Utwórz encję użytkownika
    u, err := CreateUser(ctx, client)
    if err != nil {
        log.Fatalf("nie udało się utworzyć użytkownika: %v", err)
    }
    log.Printf("utworzono użytkownika: %#v\n", u)
}

4.3. Zapytania dotyczące Encji

Aby wykonywać zapytania dotyczące encji, można użyć generatora zapytań wygenerowanego przez ent. Poniższy kod przedstawia, jak zapytać o użytkownika o nazwie "a8m":

// Funkcja QueryUser służy do wykonywania zapytania dotyczącego encji User z określoną nazwą
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Użyj client.User.Query(), aby zbudować zapytanie dotyczące User,
    // następnie łącz metodę Where, aby dodać warunki zapytania, takie jak zapytanie według nazwy użytkownika
    u, err := client.User.
        Query().
        Where(user.NameEQ("a8m")).      // Dodaj warunek zapytania, w tym przypadku nazwa to "a8m"
        Only(ctx)                      // Metoda Only oznacza, że oczekiwany jest tylko jeden wynik
    if err != nil {
        return nil, fmt.Errorf("nie udało się zapytać użytkownika: %w", err)
    }
    log.Println("zwrócono użytkownika: ", u)
    return u, nil
}

W funkcji main możesz wywołać funkcję QueryUser, aby zapytać o encję użytkownika.

func main() {
    // ... Pominięto kod dotyczący ustanowienia połączenia z bazą danych i tworzenia użytkownika

    // Zapytaj o encję użytkownika
    u, err := QueryUser(ctx, client)
    if err != nil {
        log.Fatalf("nie udało się zapytać użytkownika: %v", err)
    }
    log.Printf("zapytany użytkownik: %#v\n", u)
}

5.1. Zrozumienie krawędzi i krawędzi odwrotnej

W frameworku ent, model danych jest wizualizowany jako struktura grafu, gdzie encje reprezentują węzły w grafie, a relacje między encjami są reprezentowane przez krawędzie. Krawędź to połączenie z jednej encji do drugiej, na przykład Użytkownik może posiadać wiele Samochodów.

Krawędzie odwrotne to odwrócone odwołania do krawędzi, logicznie reprezentujące odwrotny związek między encjami, ale nie tworzące nowego związku w bazie danych. Na przykład za pomocą krawędzi odwrotnej od Samochodu możemy znaleźć Użytkownika, który jest właścicielem tego samochodu.

Kluczowe znaczenie krawędzi i krawędzi odwrotnej polega na uczynieniu nawigacji między powiązanymi encjami bardzo intuicyjną i prostą.

Wskazówka: W ent, krawędzie odpowiadają tradycyjnym kluczom obcym w bazie danych i służą do definiowania relacji między tabelami.

5.2. Definiowanie krawędzi w schemacie

Najpierw użyjemy interfejsu wiersza poleceń ent do utworzenia początkowego schematu dla Samochodu i Grupy:

go run -mod=mod entgo.io/ent/cmd/ent new Car Group

Następnie w schemacie Użytkownika definiujemy krawędź z Samochodem w celu reprezentacji relacji między użytkownikami a samochodami. Możemy dodać krawędź cars wskazującą na typ Samochód w encji użytkownika, co wskazuje, że użytkownik może mieć wiele samochodów:

// entdemo/ent/schema/user.go

// Krawędzie użytkownika.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("cars", Car.Type),
    }
}

Po zdefiniowaniu krawędzi musimy ponownie uruchomić go generate ./ent w celu wygenerowania odpowiadającego kodu.

5.3. Operacje na danych krawędzi

Tworzenie samochodów powiązanych z użytkownikiem jest prostym procesem. Mając encję użytkownika, możemy utworzyć nową encję samochodu i powiązać ją z użytkownikiem:

import (
    "context"
    "log"
    "entdemo/ent"
    // Upewnij się, że importowany jest schemat definicji Samochodu
    _ "entdemo/ent/schema"
)

func CreateCarsForUser(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("nie udało się uzyskać użytkownika: %v", err)
        return err
    }

    // Utwórz nowy samochód i powiąż go z użytkownikiem
    _, err = client.Car.
        Create().
        SetModel("Tesla").
        SetRegisteredAt(time.Now()).
        SetOwner(user).
        Save(ctx)
    if err != nil {
        log.Fatalf("nie udało się utworzyć samochodu dla użytkownika: %v", err)
        return err
    }

    log.Println("samochód został utworzony i powiązany z użytkownikiem")
    return nil
}

Podobnie, zapytanie o samochody użytkownika jest proste. Jeśli chcemy pobrać listę wszystkich samochodów należących do użytkownika, możemy to zrobić w następujący sposób:

func QueryUserCars(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("nie udało się uzyskać użytkownika: %v", err)
        return err
    }

    // Zapytaj o wszystkie samochody należące do użytkownika
    cars, err := user.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("nie udało się zapytać o samochody: %v", err)
        return err
    }

    for _, car := range cars {
        log.Printf("samochód: %v, model: %v", car.ID, car.Model)
    }
    return nil
}

Poprzez powyższe kroki nie tylko nauczyliśmy się definiować krawędzie w schemacie, ale także pokazaliśmy, jak tworzyć i zapytywać dane związane z krawędziami.

6. Przejścia Grafu i Zapytania

6.1. Zrozumienie Struktur Grafu

W ent, struktury grafowe są reprezentowane przez encje i krawędzie między nimi. Każda encja jest równoważna węzłowi w grafie, a relacje między encjami są reprezentowane przez krawędzie, które mogą być jeden do jednego, jeden do wielu, wiele do wielu, itp. Ta struktura grafowa sprawia, że ​​złożone zapytania i operacje na bazie danych relacyjnej są proste i intuicyjne.

6.2. Przechodzenie Struktur Graficznych

Pisanie kodu do przechodzenia grafów polega głównie na odpytywaniu i łączeniu danych poprzez krawędzie pomiędzy encjami. Poniżej znajduje się prosty przykład demonstrujący, jak przechodzić przez strukturę grafu w ent:

import (
    "context"
    "log"

    "entdemo/ent"
)

// PrzechodzenieGrafu to przykład przechodzenia przez strukturę grafu
func PrzechodzenieGrafu(ctx context.Context, klient *ent.Client) error {
    // Odpytaj użytkownika o nazwie "Ariel"
    a8m, err := klient.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
    if err != nil {
        log.Fatalf("Błąd odpytywania użytkownika: %v", err)
        return err
    }

    // Przejdź przez wszystkie samochody należące do Ariela
    samochody, err := a8m.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("Błąd odpytywania samochodów: %v", err)
        return err
    }
    for _, samochod := range samochody {
        log.Printf("Ariel ma samochód o modelu: %s", samochod.Model)
    }

    // Przejdź przez wszystkie grupy, do których należy Ariel
    grupy, err := a8m.QueryGroups().All(ctx)
    if err != nil {
        log.Fatalf("Błąd odpytywania grup: %v", err)
        return err
    }
    for _, g := range grupy {
        log.Printf("Ariel jest członkiem grupy: %s", g.Name)
    }

    return nil
}

Powyzszy kod stanowi podstawowy przykład przechodzenia grafu, który najpierw odpytuje użytkownika, a następnie przechodzi przez samochody i grupy użytkownika.

7. Wizualizacja Schematu Bazy Danych

7.1. Instalacja Narzędzia Atlas

Aby wizualizować schemat bazy danych wygenerowany przez ent, możemy użyć narzędzia Atlas. Proces instalacji narzędzia Atlas jest bardzo prosty. Na przykład na macOS możesz zainstalować je za pomocą brew:

brew install ariga/tap/atlas

Uwaga: Atlas to uniwersalne narzędzie migracji bazy danych, które może zarządzać wersjonowaniem struktury tabel dla różnych baz danych. Szczegółowe wprowadzenie do Atlas zostanie przedstawione w późniejszych rozdziałach.

7.2. Generowanie Diagramu ERD i Schematu SQL

Korzystanie z narzędzia Atlas do przeglądania i eksportowania schematów jest bardzo proste. Po zainstalowaniu Atlasa, możesz użyć poniższej komendy, aby zobaczyć Diagram Encji-Stosunków (ERD):

atlas schema inspect -d [database_dsn] --format dot

Lub bezpośrednio wygenerować schemat SQL:

atlas schema inspect -d [database_dsn] --format sql

Gdzie [database_dsn] wskazuje na nazwę źródła danych (DSN) twojej bazy danych. Na przykład dla bazy danych SQLite może to być:

atlas schema inspect -d "sqlite://file:ent.db?mode=memory&cache=shared" --format dot

Wygenerowany wynikowy kod może być dalej przekształcony na widoki lub dokumenty za pomocą odpowiednich narzędzi.

8. Migracja Schematu

8.1. Automatyczna Migracja i Migracja Wersjonowana

ent obsługuje dwie strategie migracji schematu: automatyczną migrację oraz migrację wersjonowaną. Automatyczna migracja polega na inspekcji i aplikowaniu zmian w schemacie w czasie wykonania, nadaje się do zastosowań w czasie rozwoju i testów. Migracja wersjonowana polega na generowaniu skryptów migracji i wymaga starannej weryfikacji i testowania przed wdrożeniem do produkcji.

Wskazówka: W przypadku automatycznej migracji, odwołaj się do treści w sekcji 4.1.

8.2. Wykonywanie Migracji Wersjonowanej

Proces migracji wersjonowanej polega na generowaniu plików migracji za pomocą Atlasa. Poniżej znajdują się odpowiednie komendy:

Dla generowania plików migracji:

atlas migrate diff -d ścieżka/do/schematu/ent --dir ścieżka/do/migracji

Następnie te pliki migracji mogą zostać zastosowane do bazy danych:

atlas migrate apply -d ścieżka/do/migracji --url database_dsn

Po wykonaniu tego procesu możesz zarządzać historią migracji bazy danych w systemie kontroli wersji i zapewnić dokładną weryfikację przed każdą migracją.

Wskazówka: Odwołaj się do przykładowego kodu na stronie https://github.com/ent/ent/tree/master/examples/start