1. Введение в ent
Ent - это фреймворк сущностей, разработанный Facebook специально для языка Go. Он упрощает процесс создания и поддержки приложений для модели данных большого масштаба. Фреймворк ent в основном следует следующим принципам:
- Легко моделировать схему базы данных в виде графовой структуры.
- Определение схемы в виде кода на языке Go.
- Реализация статических типов на основе генерации кода.
- Написание запросов к базе данных и обход графа очень просто.
- Легко расширяем и настраиваем с использованием шаблонов Go.
2. Настройка среды
Для начала использования фреймворка ent убедитесь, что язык Go установлен в вашей среде разработки.
Если ваш каталог проекта находится вне GOPATH
, или если вы не знакомы с GOPATH
, вы можете использовать следующую команду для создания нового проекта модуля Go:
go mod init entdemo
Это инициализирует новый модуль Go и создаст новый файл go.mod
для вашего проекта entdemo
.
3. Определение первой схемы
3.1. Создание схемы с использованием ent CLI
Сначала вам нужно выполнить следующую команду в корневом каталоге вашего проекта, чтобы создать схему с именем User с помощью инструмента командной строки ent CLI:
go run -mod=mod entgo.io/ent/cmd/ent new User
Вышеуказанная команда сгенерирует схему User в каталоге entdemo/ent/schema/
:
Файл entdemo/ent/schema/user.go
:
package schema
import "entgo.io/ent"
// User содержит определение схемы для сущности User.
type User struct {
ent.Schema
}
// Поля User.
func (User) Fields() []ent.Field {
return nil
}
// Рёбра User.
func (User) Edges() []ent.Edge {
return nil
}
3.2. Добавление полей
Затем нам нужно добавить определения полей в схему User. Ниже приведен пример добавления двух полей в сущность User.
Измененный файл entdemo/ent/schema/user.go
:
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// Поля User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
Этот фрагмент кода определяет два поля для модели User: age
и name
, где age
- это положительное целое число, а name
- строка со значением по умолчанию "unknown".
3.3. Генерация сущностей базы данных
После определения схемы вам нужно выполнить команду go generate
, чтобы сгенерировать логику доступа к базе данных.
Выполните следующую команду в корневом каталоге вашего проекта:
go generate ./ent
Эта команда сгенерирует соответствующий код на языке Go на основе ранее определенной схемы, что приведет к следующей структуре файлов:
ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... (несколько файлов опущены для краткости)
├── 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. Установка соединения с базой данных
Для установки соединения с базой данных MySQL мы можем использовать функцию Open
, предоставленную фреймворком ent
. Сначала импортируем драйвер MySQL, а затем предоставляем правильную строку подключения для инициализации соединения с базой данных.
package main
import (
"context"
"log"
"entdemo/ent"
_ "github.com/go-sql-driver/mysql" // Импортируем драйвер MySQL
)
func main() {
// Используем ent.Open для установления соединения с базой данных MySQL.
// Не забудьте заменить заполнители "your_username", "your_password" и "your_database" ниже.
client, err := ent.Open("mysql", "your_username:your_password@tcp(localhost:3306)/your_database?parseTime=True")
if err != nil {
log.Fatalf("ошибка открытия соединения с mysql: %v", err)
}
defer client.Close()
// Запускаем инструмент автоматической миграции
ctx := context.Background()
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("ошибка создания схемных ресурсов: %v", err)
}
// Дополнительную бизнес-логику можно написать здесь
}
4.2. Создание сущностей
Создание сущности User включает в себя создание нового объекта сущности и сохранение его в базу данных с использованием метода Save
или SaveX
. В следующем коде показано, как создать новую сущность User и инициализировать два поля age
и name
.
// Функция CreateUser используется для создания новой сущности User
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
// Используйте client.User.Create() для создания запроса на создание User,
// затем цепляйте методы SetAge и SetName для установки значений полей сущности.
u, err := client.User.
Create().
SetAge(30). // Установить возраст пользователя
SetName("a8m"). // Установить имя пользователя
Save(ctx) // Вызываем Save для сохранения сущности в базе данных
if err != nil {
return nil, fmt.Errorf("ошибка создания пользователя: %w", err)
}
log.Println("пользователь был создан: ", u)
return u, nil
}
В функции main
вы можете вызвать функцию CreateUser
для создания новой пользовательской сущности.
func main() {
// ...Пропущен код установления соединения с базой данных
// Создание пользовательской сущности
u, err := CreateUser(ctx, client)
if err != nil {
log.Fatalf("ошибка создания пользователя: %v", err)
}
log.Printf("созданный пользователь: %#v\n", u)
}
4.3. Запрос сущностей
Для запроса сущностей мы можем использовать построитель запросов, сгенерированный ent
. В следующем коде показано, как запросить пользователя с именем "a8m":
// Функция QueryUser используется для запроса сущности пользователя с указанным именем
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
// Используйте client.User.Query() для создания запроса для User,
// затем цепляйте метод Where для добавления условий запроса, таких как запрос по имени пользователя
u, err := client.User.
Query().
Where(user.NameEQ("a8m")). // Добавляем условие запроса: в данном случае имя "a8m"
Only(ctx) // Метод Only указывает, что ожидается только один результат
if err != nil {
return nil, fmt.Errorf("ошибка запроса пользователя: %w", err)
}
log.Println("возвращен пользователь: ", u)
return u, nil
}
В функции main
вы можете вызвать функцию QueryUser
для запроса сущности пользователя.
func main() {
// ...Пропущен код установления соединения с базой данных и создания пользователя
// Запрос сущности пользователя
u, err := QueryUser(ctx, client)
if err != nil {
log.Fatalf("ошибка запроса пользователя: %v", err)
}
log.Printf("запрошенный пользователь: %#v\n", u)
}
5.1. Понимание рёбер и обратных рёбер
В фреймворке ent
модель данных визуализируется в виде графовой структуры, где сущности представляют собой узлы в графе, а отношения между сущностями представлены рёбрами. Ребро представляет собой связь от одной сущности к другой, например, Пользователь
может владеть несколькими Автомобилями
.
Обратные рёбра - это обратные ссылки на рёбра, логически представляющие обратные отношения между сущностями, но не создают нового отношения в базе данных. Например, через обратное ребро Автомобиля
мы можем найти Пользователя
, владеющего этим автомобилем.
Основное значение рёбер и обратных рёбер заключается в том, что они делают навигацию между связанными сущностями очень интуитивной и простой.
Совет: В
ent
рёбра соответствуют традиционным внешним ключам базы данных и используются для определения отношений между таблицами.
5.2. Определение рёбер в схеме
Сначала мы будем использовать интерфейс командной строки ent
для создания начальной схемы для Car
и Group
:
go run -mod=mod entgo.io/ent/cmd/ent new Car Group
Затем в схеме User
мы определим ребро с Car
для представления отношения между пользователями и автомобилями. Мы можем добавить ребро cars
, указывающее на тип Car
в сущности пользователя, указывая на то, что у пользователя может быть несколько автомобилей:
// entdemo/ent/schema/user.go
// Рёбра пользователя.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}
После определения рёбер нам необходимо снова выполнить go generate ./ent
, чтобы сгенерировать соответствующий код.
5.3. Работа с данными рёбер
Создание автомобилей, связанных с пользователем, является простым процессом. Учитывая сущность пользователя, мы можем создать новую сущность автомобиля и связать её с пользователем:
import (
"context"
"log"
"entdemo/ent"
// Убедитесь, что импортировано определение схемы для автомобиля
_ "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("не удалось получить пользователя: %v", err)
return err
}
// Создание нового автомобиля и связь его с пользователем
_, err = client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()).
SetOwner(user).
Save(ctx)
if err != nil {
log.Fatalf("не удалось создать автомобиль для пользователя: %v", err)
return err
}
log.Println("автомобиль был создан и связан с пользователем")
return nil
}
Точно так же запрос списка автомобилей пользователя прост: если мы хотим извлечь список всех автомобилей, принадлежащих пользователю, мы можем сделать следующее:
func QueryUserCars(ctx context.Context, client *ent.Client, userID int) error {
user, err := client.User.Get(ctx, userID)
if err != nil {
log.Fatalf("не удалось получить пользователя: %v", err)
return err
}
// Запрос всех автомобилей, принадлежащих пользователю
cars, err := user.QueryCars().All(ctx)
if err != nil {
log.Fatalf("не удалось выполнить запрос автомобилей: %v", err)
return err
}
for _, car := range cars {
log.Printf("автомобиль: %v, модель: %v", car.ID, car.Model)
}
return nil
}
Через вышеперечисленные шаги мы не только узнали, как определить рёбра в схеме, но и продемонстрировали, как создавать и запрашивать данные, связанные с рёбрами.
6. Обход графа и запросы
6.1. Понимание графовых структур
В ent
графовые структуры представлены сущностями и рёбрами между ними. Каждая сущность эквивалентна узлу в графе, а отношения между сущностями представлены рёбрами, которые могут быть один-к-одному, один-ко-многим, многие-ко-многим и т. д. Эта структура графа делает сложные запросы и операции с реляционной базой данных простыми и интуитивными.
6.2. Обход графических структур
При написании кода для обхода графа в основном осуществляется запрос и ассоциация данных через связи между сущностями. Ниже приведен простой пример демонстрации того, как осуществлять обход графической структуры в ent
:
import (
"context"
"log"
"entdemo/ent"
)
// GraphTraversal - пример обхода графической структуры
func GraphTraversal(ctx context.Context, client *ent.Client) error {
// Запрос пользователя с именем "Ariel"
a8m, err := client.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
if err != nil {
log.Fatalf("Ошибка запроса пользователя: %v", err)
return err
}
// Обход всех машин, принадлежащих Ariel
cars, err := a8m.QueryCars().All(ctx)
if err != nil {
log.Fatalf("Ошибка запроса машин: %v", err)
return err
}
for _, car := range cars {
log.Printf("У Ариэля есть машина модели: %s", car.Model)
}
// Обход всех групп, в которых состоит Ariel
groups, err := a8m.QueryGroups().All(ctx)
if err != nil {
log.Fatalf("Ошибка запроса групп: %v", err)
return err
}
for _, g := range groups {
log.Printf("Ариэль состоит в группе: %s", g.Name)
}
return nil
}
Вышеуказанный код представляет собой базовый пример обхода графа, который сначала запрашивает пользователя, а затем обходит машины и группы пользователя.
7. Визуализация схемы базы данных
7.1. Установка инструмента Atlas
Для визуализации схемы базы данных, созданной в ent
, можно использовать инструмент Atlas. Установка Atlas очень проста. Например, на macOS его можно установить, воспользовавшись brew
:
brew install ariga/tap/atlas
Примечание: Atlas - универсальный инструмент миграции базы данных, который может управлять версионным управлением структуры таблиц для различных баз данных. Подробное введение в Atlas будет предоставлено в последующих главах.
7.2. Генерация ERD и SQL-схемы
Использование Atlas для просмотра и экспорта схем очень просто. После установки Atlas можно использовать следующую команду для просмотра диаграммы сущность-связь (ERD):
atlas schema inspect -d [database_dsn] --format dot
Или непосредственно создать SQL-схему:
atlas schema inspect -d [database_dsn] --format sql
Где [database_dsn]
указывает на источник данных (DSN) вашей базы данных. Например, для базы данных SQLite это может быть:
atlas schema inspect -d "sqlite://file:ent.db?mode=memory&cache=shared" --format dot
Сгенерированный этими командами вывод можно дополнительно преобразовать в представления или документы с использованием соответствующих инструментов.
8. Миграция схемы
8.1. Автоматическая миграция и версионная миграция
ent поддерживает две стратегии миграции схемы: автоматическая миграция и версионная миграция. Автоматическая миграция - это процесс инспектирования и применения изменений схемы во время выполнения, подходящий для разработки и тестирования. Версионная миграция включает в себя генерацию скриптов миграции и требует тщательного рассмотрения и тестирования перед развертыванием в производственную среду.
Совет: Для автоматической миграции обратитесь к содержимому раздела 4.1.
8.2. Выполнение версионной миграции
Процесс версионной миграции включает генерацию файлов миграции через Atlas. Ниже приведены соответствующие команды:
Для генерации файлов миграции:
atlas migrate diff -d ent/schema/path --dir migrations/dir
Затем эти файлы миграции можно применить к базе данных:
atlas migrate apply -d migrations/dir --url database_dsn
После этого можно вести историю миграций базы данных в системе контроля версий и обеспечить тщательное рассмотрение перед каждой миграцией.
Совет: Обратитесь к образцу кода на https://github.com/ent/ent/tree/master/examples/start