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. С помощью всего нескольких простых вызовов методов он может предварительно загружать обширные связанные данные и организовывать их структурированным образом. Это обеспечивает большое удобство при разработке приложений, основанных на данных.