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