Содержание урока · 10 разделов
Поставим задачу, на которой проедет весь курс: микроблог. Пользователи пишут посты, к постам оставляют комментарии, посты лайкают, пользователи подписываются друг на друга. В базе это четыре-пять таблиц со связями «один-ко-многим» и «многие-ко-многим», и даже один запрос «лента подписок» разворачивается в цепочку: подписки → посты авторов → авторы → счётчик лайков. На GORM такой запрос собирается из цепочек методов с именами ассоциаций внутри строк: Preload("Posts.Comments"). На чистом database/sql — руками написанный JOIN плюс ручной Scan по колонкам. И там, и там слой данных состоит из строк в кавычках, которые компилятор не читает. ent начинался именно с этого раздражения: если данные — граф, почему описание связей не сделать таким же кодом, как всё остальное в Go? В этом уроке разберём, что такое ent, как устроена его кодогенерация и чем её контракт отличается от подходов GORM и sqlc.
Связи, невидимые компилятору
GO// GORM: связи — строки внутри строк
db.Preload("Posts.Comments").
Preload("Posts.Likes").
Find(&users, "id = ?", uid)
// database/sql: рёбра собираются руками
rows, err := db.Query(`
SELECT p.id, p.title, u.name
FROM posts p
JOIN users u ON u.id = p.author_id
WHERE p.author_id = $1`, uid)Оба варианта собираются без предупреждений: для компилятора это строки. Preload("Posts.Commnets") с опечаткой внутри скомпилируется и выстрелит только при запросе, а связь users.id = posts.author_id живёт в тексте SQL, где колонки для Scan идут по порядку. Каждый новый вид связи — ещё одна строка в кавычках, и ни одна не видна ни компилятору, ни рефакторингу, ни автодополнению. В сервисе со сложным доменом именно на этом стыке копятся самые дорогие баги: не синтаксические, а расхождения между кодом и схемой.
Что такое ent: генератор поверх схемы#
ent — библиотека для работы с базой из Go, но слово «ORM» к ней стоит применять с оговоркой. GORM (мы разбирали его в вводном уроке курса) живёт в рантайме: по struct-тегам через рефлексию он в момент запроса сочиняет SQL. sqlc (вводный урок здесь) уходит в другую крайность: вы пишете SQL руками в .sql-файлах, а инструмент проверяет их по схеме и генерирует типизированную обвязку. ent — третий путь: вы описываете схему данных как обычный Go-код, а слой запросов — сущности, билдеры, предикаты — генерируется из этой схемы командой go generate. Кодогенерация как у sqlc, но источник — не SQL-запросы, а декларативное описание данных, как у ORM.
Проект вырос внутри Facebook: его начал писать Ariel Mashraki примерно в 2016 году, в 2019-м код опубликовали в открытый доступ. Сегодня ent развивается под эгидой Linux Foundation, документация живёт на entgo.io, модуль называется entgo.io/ent, а репозиторий — github.com/facebook/ent. Внутри Meta библиотека работает в продакшене, а через интеграцию entgql из схемы генерируется и GraphQL-слой — до него дойдём в конце курса. Базы: MySQL, PostgreSQL, SQLite и TiDB, все через стандартные драйверы database/sql.
Данные — это граф#
Реляционная база выглядит таблицами, но домен под ней — граф. Пользователь — узел, пост — узел, ребро «написал» соединяет их; комментарии — узлы на ребре от поста, подписки — рёбра «пользователь → пользователь». Внешние ключи и join-таблицы — это те же рёбра, просто спрятанные в колонки. GORM с ними честно работает, но статус у связей второй сорт: ассоциации объявляются тегами на полях, а в запросах — строками. ent ставит ребро в один ряд с полем: в схеме два метода — Fields для колонок и Edges для связей, и у каждой связи есть имя, тип (один-к-одному, один-ко-многим, многие-ко-многим) и поведение при удалении. Из этого решения вырастают все самые приятные вещи библиотеки: жадная загрузка WithPosts(), предикаты по связям вида HasOwnerWith(...), обход графа цепочками client.User.Query().QueryPosts().QueryComments(). Пока это выглядит как обещания — через пару блоков увидим код.
Первая схема: пользователь микроблога
GOpackage schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// User — схема пользователя микроблога.
type User struct {
ent.Schema
}
// Fields описывает колонки таблицы users.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
field.String("email").
Unique(),
field.Int("age").
Positive(),
}
}Разбор построчно. package schema — все схемы живут в одном пакете ent/schema. Тип User встраивает ent.Schema — это маркер, по которому генератор находит ваши сущности. Fields() возвращает список полей, каждое строится конструктором: field.String("name") — строковая колонка, field.Int("age") — числовая. Цепочки после конструктора — валидаторы и ограничения: NotEmpty() запрещает пустую строку, Unique() ставит уникальный индекс на колонку, Positive() не пустит отрицательный возраст. Проверка происходит в сгенерированном коде ещё до похода в базу: пользователь с пустым email вернёт ошибку валидации, а не ошибку СУБД. И обратите внимание: здесь нет ни одного тега и ни одной строки SQL — обычный Go, который компилируется, автодополняется и рефакторится как любой другой.
Схема как код: Fields, Edges и go generate#
Зачем усложнять, если GORM обходится тегами? Причины практические. Тег — это строка: опечатка в gorm:"uniqueIndex" компилятором не проверяется, и опция молча выбрасывается. Схема ent — код: неверный тип аргумента, лишний параметр, забытый валидатор отсекаются компилятором ещё до генерации. Дальше — сама генерация. В ent/generate.go лежит директива //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema, и команда go generate ./ent разворачивает схему в полноценный пакет. Появляются: ent/user.go — сущность со структурой User; ent/user_create.go, ent/user_update.go, ent/user_query.go — билдеры для каждой операции; ent/user/ — отдельный пакет предикатов для условий; ent/mutation.go — представление изменений, на котором держатся хуки; ent/client.go — входная точка, клиент базы.
Сгенерированный код принято коммитить в репозиторий — это официальная рекомендация ent: сборка не зависит от того, запустил ли разработчик генерацию, а диффы по ent/ видны в code review. Минус подхода честен: в каждом PR появляются сотни сгенерированных строк, и первые дни они пугают. Привыкание занимает день-два, зато дальше вы читаете не весь пакет, а только сигнатуры предикатов и билдеров — остальное стабильнее самого себя.
Edges: пользователь пишет посты
GO// Edges of the User: исходящее ребро «один-ко-многим».
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("posts", Post.Type),
}
}
// Post: обратная сторона того же ребра.
func (Post) Edges() []ent.Edge {
return []ent.Edge{
edge.From("author", User.Type).
Ref("posts").
Unique(),
}
}edge.To("posts", Post.Type) объявляет ребро от пользователя к постам: без Unique() это «один-ко-многим» — у пользователя много постов. С обратной стороны пост ссылается на автора: edge.From("author", User.Type).Ref("posts").Unique() — «многие-к-одному», у поста ровно один автор. Ref связывает две половины ребра по именам, и генератор проверяет соответствие при генерации: описать половинки, которые не сходятся, не выйдет. Внешний ключ генератор разместит в таблице posts. Из имён рёбер рождаются методы: WithPosts() и WithAuthor() для жадной загрузки, HasPosts() и HasAuthor() для предикатов, QueryPosts() для обхода графа. Имена рёбер — часть публичного API вашего слоя данных: они попадают прямо в компилятор.
Запрос builder-стилем
GOclient, err := ent.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
return err
}
defer client.Close()
ctx := context.Background()
users, err := client.User.
Query().
Where(
user.NameEQ("Анна"),
user.AgeGTE(18),
).
WithPosts().
All(ctx)ent.Open(диалект, dsn) открывает клиент: первый аргумент — диалект (postgres, mysql, sqlite3, tidb), второй — строка подключения. client.User — сгенерированный доступ к сущности; .Query() возвращает билдер, у которого всё автодополняется. Where() принимает предикаты из сгенерированного пакета ent/user: NameEQ("Анна") — точное равенство, AgeGTE(18) — «больше или равно». Несколько предикатов в Where объединяются через И, а переиспользовать их можно как обычные значения — предикат это переменная, его можно вернуть из функции и передать дальше (систематически — в десятом уроке). WithPosts() жадно загружает посты дополнительным запросом, All(ctx) исполняет и возвращает срез сущностей. В этой цепочке нет ни одной строки-условия: опечатка в любом методе — ошибка компиляции.
Типобезопасность: компилятор против рантайма#
Самая дорогая особенность ent видна не в красивом коде, а в сценарии переименования. Допустим, поле age в схеме переименовали в birth_year. В GORM поле структуры и строка Where("age = ?") расходятся молча: код собирается, библиотека в рантайме отправит запрос со старым именем, и ошибку вернёт драйвер — в лучшем случае на тестовом стенде, в худшем в проде. В ent цепочка другая: правите схему, запускаете go generate — и все вызовы user.AgeGTE() перестают компилироваться, потому что такого метода больше нет. Компилятор перечислит каждый вызов по файлам, IDE предложит замену. Это тот же заслон, что у sqlc (запросы проверяются по схеме на этапе генерации), только не для SQL-файлов, а для всего слоя данных целиком: связей, загрузок, вставок.
Из этого следует дисциплина всего курса: поменяли схему — сразу generate, пока сгенерированный код и вызывающий код не разошлись. Кто забывает, получает не рантайм-ошибку в проде, а волну ошибок компиляции на своей машине. Это правильный порядок вещей, к которому привыкаешь быстрее, чем кажется.
GORM, sqlc и ent по критериям#
| Критерий | GORM | sqlc | ent |
|---|---|---|---|
| Что пишет разработчик | структуры с тегами и цепочки методов | SQL в .sql-файлах | схемы (Go-код) в ent/schema |
| Кто собирает SQL | библиотека в рантайме, через рефлексию | никто: запрос ваш, обвязка сгенерирована | генератор по схеме, до запуска программы |
| Где ловится опечатка в колонке | в рантайме | на sqlc generate | на компиляции Go |
| Связи | ассоциации-теги, Preload со строками | JOIN руками | edges в схеме, With* типизировано |
| Динамические фильтры | легко: Where() с любыми условиями | трудно: COALESCE, отдельные выборки | легко: предикаты собираются в рантайме |
| Миграции | AutoMigrate | снаружи: golang-migrate, goose | automigrate и версионируемые миграции |
| Порог входа | минимальный | нужен уверенный SQL | выше всех: кодогенерация и свои концепции |
Когда ent уместен, а когда избыточен#
Вывод из таблицы короткий: ent берёт своим верхом там, где домен — действительно граф. Мой критерий — счёт связей и скорость их изменения. Микроблог из этого курса — пользователи, посты, комментарии, лайки, подписки — это пять сущностей, где половина связей «многие-ко-многим» и схема правится каждую неделю в начале проекта. Каждый такой рефакторинг в ent дешевле: поменял схему, перегенерировал, довёл код до компиляции — готово. Роли и права доступа, мульти-арендность, каталоги с категориями и атрибутами — та же история.
Честно про обратную сторону. Маленький CRUD из трёх таблиц без связей ent не нужен: GORM даст то же за вечер, без ent/ в каждом PR и без изучения нового словаря. Тяжёлые отчёты и аналитика ent не спасает — как и GORM, он генерирует запросы, которые в агрегатах всё равно допиливают сырым SQL (или держат рядом sqlc для этого слоя). И команду, которая не готова жить с кодогенерацией, ent будет раздражать — не подведёт, а именно раздражать: лишний шаг go generate, непривычные диффы, страх править «чужой» код. Моё мнение меняется только при одном условии: если связей мало и схема стабильна, разница между инструментами не стоит потраченного дня — берите то, что команда уже знает.
Браузерный интерпретатор сайта исполняет чистый Go без сети и файловой системы, а ent-коду нужны кодогенерация (go generate) и база — поэтому примеры ent в уроках не запускаются в песочнице. Для практики нужен локальный Go и база: проще всего SQLite (файл вместо сервера) или PostgreSQL в Docker. Зато интерактивы урока — тесты, «что выведет», карточки — работают прямо в браузере и дают XP.
Базовый Go: структуры, методы, ошибки, указатели — всё это есть в базовом курсе, а нулевые значения типов разбираются в уроке про переменные и константы. Понадобятся и основы SQL: таблицы, ключи, JOIN. Если вы уже проходили курс GORM или sqlc, сравнения из этого курса лягут на готовую почву.
Проверка: что такое ent
Какой путь модуля у ent?
Проверка: где ловится ошибка
В схеме ent поле age переименовали в birth_year и перегенерировали код. Что произойдёт с вызовом user.AgeGTE(18) в запросе?
Прочитайте запрос как ent
Прочитайте код и выберите, что он выведет:
posts, err := client.Post.
Query().
Where(
post.HasOwner(),
post.TitleContains("Go"),
).
All(ctx)Восстановите схему
Поля и ребро: впишите конструктор строкового поля, валидатор непустой строки и конструктор исходящего ребра.
Соберите рабочий цикл ent
Расставьте шаги от пустого модуля до первого запроса с типизированными предикатами.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: словарь ent
Что дальше по курсу#
Во втором уроке поставим ent и соберём первые схемы: ent init, структура пакета ent/schema, файл generate.go. В третьем — поля: типы field., дефолты, валидаторы, уникальность. Четвёртый — optional и nillable, то есть работа с NULL. Пятый и шестой — edges: O2M и M2O, затем M2M и кастомные join-таблицы. Седьмой — устройство генерации подробнее. Дальше по плану: запросы и предикаты, жадная загрузка With, пагинация, update/delete, миграции, транзакции, хуки и интерсепторы, privacy-политики, GraphQL через entgql, тестирование и производительность. Финал — итоговый проект: микроблог целиком, с лайками и подписками, на которых мы и отработаем граф целиком. Полный план из 25 уроков — на странице курса.
Если перед стартом захочется освежить альтернативы, вводные уроки GORM и sqlc показывают те же задачи слоя данных с обеих сторон — все сравнения в этом курсе будут опираться на них.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- ent — библиотека от Facebook с кодогенерацией: схему описываешь Go-кодом, слой запросов генерируется командой go generate ./ent.
- Данные — граф: сущности — узлы, связи — рёбра; O2M, M2O, M2M и O2O объявляются в Edges() схемы.
- Сгенерированный код — сущности, билдеры, предикаты вида user.NameEQ — коммитится в репозиторий.
- Ошибки слоя данных ловит компилятор: переименование поля роняет все вызовы user.AgeGTE, а не продакшен.
- GORM — рефлексия и теги в рантайме; sqlc — генерация из ваших SQL-запросов; ent — генерация из схемы-кода.
- ent окупается на графовых доменах со связями, как микроблог; маленький CRUD и тяжёлые отчёты — не его стихия.
- Песочница не исполняет ent: для практики нужна локальная связка Go и базы.
Домашнее задание
Поднимите стартовый модуль локально: go mod init blog, затем go get entgo.io/ent и go run -mod=mod entgo.io/ent/cmd/ent init User Post. Перенесите схему User из урока, опишите Post с полями title и body, объявите ребро posts/author и запустите go generate ./ent. Откройте ent/user/where.go и найдите предикаты NameEQ и AgeGTE. Затем переименуйте поле age в схеме, перегенерируйте и посмотрите на ошибки компиляции — это тот самый заслон, ради которого ent и делали.
Личные заметкиПусто
Вопросы и ответы по уроку
Что такое ent простыми словами?
Библиотека для Go, в которой вы описываете данные как код — схемы с полями и связями, — а она генерирует по ним типобезопасный слой запросов. Связи между сущностями — рёбра графа, запросы к ним — типизированные методы вместо строк.
Чем ent отличается от GORM?
GORM работает в рантайме через рефлексию и struct-теги, ошибки в строках условий всплывают при выполнении. ent генерирует код по схеме: опечатка в поле или связи не компилируется. GORM быстрее в старт, ent дорожает вместе с ростом графа связей.
ent или sqlc?
sqlc генерирует обвязку из ваших SQL-запросов: контроль над SQL максимален, но связей, жадной загрузки и динамических фильтров там нет. ent закрывает всё это типизированными edges и предикатами, но SQL вы не контролируете. В одном проекте их совмещают: ent для домена, sqlc для отчётов.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
go-kit: микросервисный тулкит
Курс go-kit: endpoint, транспорт, логирование, метрики, трейсинг, circuit breaker, service discovery. 25 уроков с итоговым микросервисом.
- Понимать архитектуру endpoint → transport → service
- Оборачивать сервисы middleware: логи, метрики, трейсинг
- Подключать HTTP и gRPC транспорты
- Ставить rate limiting и circuit breaker