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