1. Основные понятия сущности и связи

В фреймворке ent сущность означает основную управляемую базовую единицу данных в базе данных, которая обычно соответствует таблице в базе данных. Поля в сущности соответствуют столбцам в таблице, а связи (рёбра) между сущностями используются для описания отношений и зависимостей между сущностями. Связи между сущностями образуют основу для построения сложных моделей данных, позволяя описывать иерархические отношения, такие как отношения родитель-ребёнок и отношения собственности.

Фреймворк ent предоставляет обширный набор API, позволяющий разработчикам определять и управлять этими связями в схеме сущности. Через эти связи мы легко можем выражать и оперировать сложной бизнес-логикой между данными.

2. Типы связей сущностей в ent

2.1 Однозначная связь (O2O)

Однозначная связь означает однозначное соответствие между двумя сущностями. Например, в случае пользователей и банковских счетов каждый пользователь может иметь только один банковский счёт, и каждый банковский счёт также принадлежит только одному пользователю. Фреймворк ent использует методы edge.To и edge.From для определения таких связей.

Сначала мы можем определить однозначную связь, указывающую на Card в схеме User:

// Связи User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("card", Card.Type). // Указывает на сущность Card, определяя имя связи как "card"
            Unique(),               // Метод Unique обеспечивает, что это однозначная связь
    }
}

Затем мы определяем обратную связь обратно к User в схеме Card:

// Связи Card.
func (Card) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Указывает обратно на User от Card, определяя имя связи как "owner"
            Ref("card").              // Метод Ref указывает соответствующее обратное имя связи
            Unique(),                 // Помечено как уникальное, чтобы обеспечить соответствие одного счёта одному владельцу
    }
}

2.2 Однозначно-многозначная связь (O2M)

Однозначно-многозначная связь указывает, что одна сущность может быть связана с несколькими другими сущностями, но эти сущности могут указывать обратно только на одну сущность. Например, у пользователя может быть несколько домашних животных, но у каждого домашнего животного может быть только один владелец.

В ent мы по-прежнему используем edge.To и edge.From для определения этого типа связи. Приведенный ниже пример определяет однозначно-многозначную связь между пользователями и домашними животными:

// Связи User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("pets", Pet.Type), // Однозначно-многозначная связь от сущности User к сущности Pet
    }
}

В сущности Pet мы определяем многозначную-однозначную связь обратно к User:

// Связи Pet.
func (Pet) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Многозначно-однозначная связь от Pet к User
            Ref("pets").              // Указывает обратное имя связи от pet к owner
            Unique(),                 // Обеспечивает, что у одного владельца может быть несколько домашних животных
    }
}

2.3 Многозначно-многозначная связь (M2M)

Многозначно-многозначная связь позволяет двум типам сущностей иметь несколько экземпляров друг друга. Например, студент может записаться на несколько курсов, и курс также может иметь несколько студентов. ent предоставляет API для установления многозначных связей:

В сущности Student мы используем edge.To для установления многозначной связи с Course:

// Связи Student.
func (Student) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("courses", Course.Type), // Определяет многозначную связь от Student к Course
    }
}

Аналогично, в сущности Course мы устанавливаем обратную связь к Student для многозначной связи:

// Связи Course.
func (Course) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("students", Student.Type). // Определяет многозначную связь от Course к Student
            Ref("courses"),                  // Указывает обратное имя связи от Course к Student
    }
}

Эти типы связей являются основополагающими для построения сложных моделей данных приложений, и понимание того, как их определять и использовать в ent, критически важно для расширения моделей данных и бизнес-логики.

3. Основные операции для сущностных ассоциаций

Этот раздел продемонстрирует, как выполнять основные операции с помощью ent с определенными отношениями, включая создание, запрос и обход связанных сущностей.

3.1 Создание связанных сущностей

При создании сущностей можно одновременно устанавливать отношения между ними. Для отношений один-ко-многим (O2M) и многие-к-многим (M2M) можно использовать метод Add{Edge}, чтобы добавить связанные сущности.

Например, если у нас есть сущность пользователя и сущность питомца с определенной ассоциацией, где у пользователя может быть несколько питомцев, то ниже приведен пример создания нового пользователя и добавления для него питомцев:

// Создание пользователя и добавление питомцев
func CreateUserWithPets(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Создание экземпляра питомца
    fido := client.Pet.
        Create().  
        SetName("Фидо").
        SaveX(ctx)
    // Создание экземпляра пользователя и ассоциирование его с питомцем
    user := client.User.
        Create().
        SetName("Алиса").
        AddPets(fido). // Использование метода AddPets для ассоциации с питомцем
        SaveX(ctx)

    return user, nil
}

В этом примере мы сначала создаем экземпляр питомца с именем Фидо, затем создаем пользователя с именем Алиса и ассоциируем экземпляр питомца с пользователем с помощью метода AddPets.

3.2 Запрос связанных сущностей

Запрос связанных сущностей — это распространенная операция в ent. Например, вы можете использовать метод Query{Edge} для получения других сущностей, связанных с конкретной сущностью.

Продолжая наш пример с пользователями и питомцами, вот как запросить все питомцы, принадлежащие пользователю:

// Запрос всех питомцев пользователя
func QueryUserPets(ctx context.Context, client *ent.Client, userID int) ([]*ent.Pet, error) {
    pets, err := client.User.
        Get(ctx, userID). // Получение экземпляра пользователя на основе идентификатора пользователя
        QueryPets().      // Запрос сущностей питомцев, ассоциированных с пользователем
        All(ctx)          // Возврат всех запрошенных сущностей питомцев
    if err != nil {
        return nil, err
    }

    return pets, nil
}

В приведенном выше отрывке кода мы сначала получаем экземпляр пользователя на основе идентификатора пользователя, затем вызываем метод QueryPets для извлечения всех сущностей питомцев, связанных с этим пользователем.

Примечание: Инструмент генерации кода ent автоматически создает API для запросов ассоциаций на основе определенных отношений сущностей. Рекомендуется ознакомиться с сгенерированным кодом.

4. Предварительная загрузка

4.1 Принципы предварительной загрузки

Предварительная загрузка — это методика, применяемая при запросах к базам данных для предварительной получения и загрузки связанных сущностей. Этот подход часто используется для получения данных, связанных с несколькими сущностями за один раз, чтобы избежать множественных операций запросов к базе данных в дальнейшей обработке, что существенно повышает производительность приложения.

В фреймворке ent предварительная загрузка в основном используется для работы с отношениями между сущностями, такими как один-ко-многим и многие-ко-многим. При извлечении сущности из базы данных связанные сущности не загружаются автоматически, а загружаются явно по мере необходимости через предварительную загрузку. Это важно для решения проблемы запроса N+1 (т.е. выполнение отдельных запросов для каждой родительской сущности).

В фреймворке ent предварительная загрузка достигается с помощью метода With в построителе запросов. Этот метод генерирует соответствующие функции With... для каждого ребра, такие как WithGroups и WithPets. Эти методы автоматически генерируются фреймворком ent, и программисты могут использовать их для запроса предварительной загрузки конкретных ассоциаций.

Принцип работы предварительной загрузки сущностей заключается в том, что при запросе основной сущности ent выполняет дополнительные запросы для получения всех связанных сущностей. Затем эти сущности заполняются в поле Edges возвращаемого объекта. Это означает, что ent может выполнять несколько запросов к базе данных, по крайней мере, один раз для каждого необходимого предварительно загружаемого ребра. Хотя в определенных сценариях этот метод может быть менее эффективным, чем один сложный запрос JOIN, он предлагает большую гибкость и ожидается получение оптимизаций производительности в будущих версиях ent.

4.2 Реализация предварительной загрузки

Теперь мы продемонстрируем, как выполнять операции предварительной загрузки в фреймворке ent через примеры кода, используя модели пользователей и питомцев, описанные в обзоре.

Предварительная загрузка одного объединения

Допустим, мы хотим извлечь всех пользователей из базы данных и предварительно загрузить данные о домашних животных. Мы можем сделать это, написав следующий код:

users, err := client.User.
    Query().
    WithPets().
    All(ctx)
if err != nil {
    // Обработка ошибки
    return err
}
for _, u := range users {
    for _, p := range u.Edges.Pets {
        fmt.Printf("Пользователь (%v) владеет животным (%v)\n", u.ID, p.ID)
    }
}

В этом примере мы используем метод WithPets, чтобы запросить ent предварительно загрузить сущности домашних животных, связанных с пользователями. Предварительно загруженные данные о домашних животных заполняются в поле Edges.Pets, из которого мы можем получить доступ к этим связанным данным.

Предварительная загрузка нескольких объединений

ent позволяет нам предварительно загружать несколько объединений сразу, а также указывать предварительную загрузку вложенных объединений, фильтрацию, сортировку или ограничение количества предварительно загруженных результатов. Ниже приведен пример предварительной загрузки домашних животных администраторов и команд, к которым они принадлежат, а также предварительной загрузки пользователей, связанных с командами:

admins, err := client.User.
    Query().
    Where(user.Admin(true)).
    WithPets().
    WithGroups(func(q *ent.GroupQuery) {
        q.Limit(5)          // Ограничить до первых 5 команд
        q.Order(ent.Asc(group.FieldName)) // Сортировать по возрастанию по названию команды
        q.WithUsers()       // Предварительная загрузка пользователей в команде
    }).
    All(ctx)
if err != nil {
    // Обработка ошибок
    return err
}
for _, admin := range admins {
    for _, p := range admin.Edges.Pets {
        fmt.Printf("Администратор (%v) владеет животным (%v)\n", admin.ID, p.ID)
    }
    for _, g := range admin.Edges.Groups {
        fmt.Printf("Администратор (%v) принадлежит к команде (%v)\n", admin.ID, g.ID)
        for _, u := range g.Edges.Users {
            fmt.Printf("В команде (%v) есть участник (%v)\n", g.ID, u.ID)
        }
    }
}

Через этот пример вы можете увидеть, насколько мощен и гибок ent. С помощью всего нескольких простых вызовов методов он может предварительно загружать обширные связанные данные и организовывать их структурированным образом. Это обеспечивает большое удобство при разработке приложений, основанных на данных.