Содержание урока · 11 разделов
Маршрут урока — от первой обёртки до фильтра, который работает сам. Сначала — сигнатура: ent.Interceptor, ent.Querier и адаптер ent.InterceptFunc, и почему забыть next.Query — значит заглушить чтение целиком. Затем — регистрация на трёх уровнях: client.Intercept на весь граф, client.User.Intercept на один тип и Interceptors() в схеме. Третья часть — фича-флаг intercept и сгенерированный пакет intercept: intercept.Func, intercept.TraverseFunc и типизированные траверзеры вроде intercept.TraverseUser. Четвёртая — главный кейс: ent интерсепторы закрывают дыру мягкого удаления, которую четырнадцатый урок оставил вам как ручной труд: девять забытых предикатов — девять утечек. Пятая — дрейф запросов: где именно в конвейере чтения крепится фильтр, чтобы он доезжал до обходов рёбер и Count. Финал — границы: интерсептор фильтрует, но не решает; за решениями «можно ли» — privacy-политики девятнадцатого урока. Если вы искали «ent interceptors» или «перехват ent» — это тот урок.
Сигнатура: Interceptor, Querier, InterceptFunc#
ent interceptors — зеркальное отражение хуков из семнадцатого урока: там был ent.Mutator, здесь ent.Querier; там next.Mutate, здесь next.Query. Интерфейс ent.Interceptor — один метод: Intercept(next ent.Querier) ent.Querier. Интерфейс ent.Querier — тоже один метод: Query(ctx context.Context, query ent.Query) (ent.Value, error). Читается так: интерсептор получает следующий шаг конвейера чтения и возвращает обёртку над ним; вызов next.Query(ctx, query) означает «передай запрос дальше». Забыть этот вызов — значит остановить конвейер: выборка не дойдёт до SQL, клиент повиснет или вернёт пусто — и никакой ошибки вы не увидите, просто тихое «ничего не происходит». Для лаконичности есть адаптеры: ent.InterceptFunc оборачивает функцию вида func(next ent.Querier) ent.Querier в полноценный Interceptor, а ent.QuerierFunc — функцию тела в Querier. Внутри вы получаете query — generic-интерфейс билдера: у него нет Where и Limit, доступ к конкретике даёт type assertion — if uq, ok := query.(*ent.UserQuery); ok. ent.Value — результат: срез сущностей, число Count, скаляр Select. Регистрация: client.Intercept — рантайм-интерсепторы на все типы графа, client.User.Intercept — на один тип, func (User) Interceptors() []ent.Interceptor — на уровне схемы: как Hooks() из семнадцатого урока, встраивается в сгенерированный код и работает у всех клиентов без исключений.
Первый интерсептор: лог чтения
GOclient.Intercept(ent.InterceptFunc(func(next ent.Querier) ent.Querier {
return ent.QuerierFunc(func(ctx context.Context,
query ent.Query) (ent.Value, error) {
started := time.Now()
// next.Query — обязательный хвост: передача дальше
v, err := next.Query(ctx, query)
log.Printf("запрос %T: %v, err=%v", query, time.Since(started), err)
return v, err
})
}))client.Intercept ставит интерсептор на все типы графа: users, posts, groups — любой Query пройдёт через обёртку. ent.InterceptFunc — адаптер функции в Interceptor, ent.QuerierFunc — тела в Querier. %T печатает конкретный тип билдера (ent.UserQuery, ent.PostQuery) — готовый профиль чтения приложения. next.Query — обязательный хвост: без него конвейер мёртв.
Проверка: момент исполнения
Когда исполняется интерсептор, зарегистрированный через client.Intercept?
Три уровня регистрации#
Регистрация повторяет карту хуков из семнадцатого урока, и тот же принцип «схема — место политик, рантайм — место настроек» работает здесь без поправок. client.Intercept(...) — рантайм-интерсепторы всего графа: логи, метрики, лимиты — всё, что не зависит от типа сущности. client.User.Intercept(...) — на один тип: фильтры конкретной схемы, специфичные правки билдера. func (User) Interceptors() []ent.Interceptor — уровень схемы: интерсептор встраивается в сгенерированный код при сборке клиента и исполняется раньше любого рантайм-хука; политика мягкого удаления обязана жить именно здесь, чтобы ни один новый сервис, собравший клиент из ваших схем, не получил выборку без фильтра. Оговорка из документации, знакомая вам по схемным хукам: раз Interceptors() объявлен в схеме — добавьте в main пустой импорт import _ «ваш-модуль/ent/runtime», иначе между схемой и сгенерированным пакетом ent возможен цикл импортов, а переменные рантайма не инициализируются. Порядок внутри одного уровня — порядок регистрации: первый зарегистрированный — внешний слой конвейера, фактическое исполнение SQL — центр луковицы.
Каталог: API интерсепторов#
| Инструмент | Что даёт | Нюанс |
|---|---|---|
| ent.Interceptor | интерфейс Intercept(next ent.Querier) ent.Querier | реализуйте структурой или берите адаптер |
| ent.InterceptFunc | адаптер func(next ent.Querier) ent.Querier в Interceptor | литерал-функция без объявления своего типа |
| ent.Querier | Query(ctx, ent.Query) (ent.Value, error) — следующий шаг | next.Query — обязательный хвост каждого интерсептора |
| ent.QuerierFunc | адаптер тела в Querier | обёртка логики внутри InterceptFunc |
| ent.Query | generic-интерфейс билдера запроса | конкретика через type assertion *ent.UserQuery |
| ent.Value | результат: срез сущностей, число, скаляр | то, что вернут All, Count, Only |
| client.Intercept | регистрация на все типы графа (рантайм) | логи, метрики, лимиты по умолчанию |
| client.User.Intercept | регистрация на один тип | фильтры и правки конкретной схемы |
| func (User) Interceptors() | регистрация на уровне схемы | работает у всех клиентов; нужен import _ ent/runtime |
| intercept.TraverseUser | типизированный траверзер (фича intercept) | правит билдер на каждом шаге обхода графа |
Фича-флаг intercept и пакет intercept#
Интерсепторы в ent включаются фича-флагом — так же, как privacy в девятнадцатом уроке: без него генератор не выдаст ни часть API, ни сгенерированный пакет. В ent/generate.go добавьте --feature intercept к команде generate: go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature intercept ./schema — и запустите go generate из седьмого урока. Флаг приносит сгенерированный пакет intercept — аналог пакета hook для мутаций. Три его звезды. intercept.Func — generic-интерсептор: получает q intercept.Query и правит его без type assertion — один код на все типы графа. intercept.TraverseFunc — траверзер: исполняется не один раз в финале, а на каждом шаге обхода графа. intercept.TraverseUser — типизированный траверзер User: func(ctx, q *ent.UserQuery) error — внутри вам доступен настоящий билдер со всеми Where-предикатами из десятого урока, включая семейство IsNil. Родственный хелпер — ent.QueryFromContext(ctx): достаёт из контекста снапшот запроса, чтобы проверить, задал ли клиент Limit явно, и не перебивать его. Всё это — сахар над той же механикой Intercept/Querier: sugar не отменяет сигнатуру, а экономит type assertion.
Мягкое удаление через интерсептор
GO// ent/generate.go — включить фичу:
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature intercept ./schema
// вшить фильтр живых во все выборки User (место — схема):
func (User) Interceptors() []ent.Interceptor {
return []ent.Interceptor{
intercept.TraverseUser(func(ctx context.Context, q *ent.UserQuery) error {
q.Where(user.DeletedAtIsNil()) // фильтр живых строк
return nil
}),
}
}Рецепт из четырнадцатого урока, доведённый до автомата: там DeletedAtIsNil обязан был стоять в каждом читающем запросе вручную, и один забытый Where пропускал веру в ленту. Здесь фильтр вшивается в схеме один раз и применяется ко всем выборкам User — All, First, Only, Get по id, Count, Exist — у всех клиентов графа. return nil — траверзер «без мнения»: правит билдер и пропускает запрос дальше.
Проверка: Get по id
Интерсептор с user.DeletedAtIsNil() зарегистрирован. Что вернёт client.User.Get(ctx, id) для «удалённой» веры?
Дрейф запросов: где крепится фильтр#
Запрос в приложении дрейфует: хендлер собирает билдер, на него насаживаются предикаты, сортировки и With-загрузки из одиннадцатого урока, затем запрос уходит в обходы рёбер и подзапросы, и лишь в конце исполняется. Фильтр, вшитый в одном месте кода, не обязан доезжать до всех веток этого дрейфа — и вот четыре уровня крепления, от самого хрупкого к самому надёжному. Первый — ручной Where в каждом вызове: работает, пока их девять, утечка гарантирована на десятом. Второй — интерсептор типа: исполняется один раз на финальной стадии, правит финальный билдер конкретного типа; для фильтрации одной сущности этого достаточно. Третий — траверзер: исполняется на каждом шаге обхода графа, поэтому фильтр User доезжает и до промежуточных запросов: client.User.Query().QueryGroups() — фильтр применён на шаге User, ещё до того, как выборка групп начнёт собираться. Четвёртый — всё то же самое, но в схеме: гарантия для всех клиентов. Разница интерсептора и траверзера — момент вмешательства: интерсептор — middleware финальной стадии, он видит запрос перед исполнением и строки после; траверзер — на каждом шаге дрейфа, он правит и промежуточные, и финальные запросы до их склейки. Фильтры по умолчанию — траверзеры; логирование и кеш — интерсепторы.
Траверзер на обходах: посты только живых
GO// вера «удалена» через SetDeletedAt вчера; у анны 2 поста,
// у бориса 1, у веры 1 — всего 4.
n := client.User.Query().QueryPosts().CountX(ctx) // 4: обход видит всех
client.User.Intercept(
intercept.TraverseUser(func(ctx context.Context, q *ent.UserQuery) error {
q.Where(user.DeletedAtIsNil())
return nil
}),
)
n = client.User.Query().QueryPosts().CountX(ctx) // 3: вера отфильтрованаОбход QueryPosts() — это чтение User и затем Post; траверзер User срабатывает на первом шаге, вера вылетает из промежуточной выборки, и её пост не доезжает до итога. Без траверзера CountX вернул бы 4 — «удалённый» контент считался в статистике. Заметьте: для фильтрации самих постов нужен траверзер Post — пост веры исчез из обхода лишь потому, что исчезла вера.
Count до и после траверзера
Прочитайте код и выберите, что он выведет:
// анна — 2 поста, борис — 1, вера — 1 (вера SetDeletedAt вчера).
client.User.Intercept(
intercept.TraverseUser(func(ctx context.Context, q *ent.UserQuery) error {
q.Where(user.DeletedAtIsNil())
return nil
}),
)
n := client.User.Query().CountX(ctx) // A
m := client.Post.Query().CountX(ctx) // BГраницы: что интерсептор не делает#
Четыре границы честности. Первая: интерсепторы применяются только к чтению — на мутации они не подписаны, там территория хуков из семнадцатого урока; UpdateOneID не зайдёт в Intercept ни при каких условиях. Вторая: интерсептор фильтрует, но не решает — он прячет строки из выборок, но не отвечает на вопрос «имеет ли этот viewer право вообще»: если клиент без ограничений может вызвать UpdateOneID — фильтр чтения его не остановит; решения Allow/Deny — работа privacy-политик девятнадцатого урока, и на пути запроса политика исполняется раньше интерсепторов. Третья: интерсептор не обязан срабатывать на агрегатах вашей версии — Count и Select в современных версиях проходят через тот же конвейер с интерсепторами, но GroupBy и кастомные Modify-пути исторически были слепыми пятнами; не рассчитывайте вслепую — проверьте на своей версии ent тестом на три строки. Четвёртая: With-загрузки из одиннадцатого урока исполняются через клиент целевого типа, поэтому фильтр User применяется и к WithPosts, и к WithOwner — но помните про дрейф: пост веры пропадёт из WithPosts анны только если вера отфильтрована на своём шаге, а посты всех «живых» авторов останутся в полном составе.
Лимит по умолчанию: intercept.Func
GOclient.Intercept(
intercept.Func(func(ctx context.Context, q intercept.Query) error {
// если клиент не задал Limit явно — режем до 1000:
if ent.QueryFromContext(ctx).Limit == nil {
q.Limit(1000)
}
return nil
}),
)intercept.Func — generic-интерсептор без type assertion: q intercept.Query понимает Limit на любом типе графа. ent.QueryFromContext(ctx) достаёт снапшот запроса, где видно, вызывал ли клиент Limit сам: незаданный лимит становится 1000, заданный не трогается. Классический страховочный паттерн: один потерянный Where — утечка строк, один потерянный Limit — утечка памяти и таймауты.
Проверка: интерсепторы и мутации
Зарегистрирован интерсептор с user.DeletedAtIsNil(). Что произойдёт при client.User.DeleteOneID(1).Exec(ctx)?
Чеклист интерсепторов#
- next.Query(ctx, query) — обязательный хвост: без него выборка умирает на этапе конвейера, без ошибок и без SQL.
- Схема раньше рантайма: политика фильтрации — в func (User) Interceptors(), настройки конкретного приложения — в client.Intercept; не забудьте import _ ent/runtime.
- Фильтры по умолчанию — траверзеры (intercept.TraverseFunc, intercept.TraverseUser): они исполняются на каждом шаге дрейфа запроса и доезжают до обходов рёбер.
- Логирование, метрики и лимиты — интерсепторы (intercept.Func): им достаточно финальной стадии исполнения.
- Фильтр одного типа не фильтрует соседние: траверзер User не режет client.Post.Query() — пишите симметричный траверзер Post.
- Get по id проходит через те же интерсепторы: NotFoundError на «удалённой» строке — ожидаемое поведение, а не баг.
- Интерсептор не решает доступ: фильтр прячет строки, но не запрещает операции — за Allow/Deny ходите в privacy-политики девятнадцатого урока.
- Проверяйте на своей версии агрегатные пути (GroupBy, Modify): у интерсепторов там исторически бывали слепые пятна.
Runner-ов в курсе ent нет, и песочница не появится и здесь: интерсепторам нужны кодогенерация и база — правило всего курса. Соберите мягкое удаление из четырнадцатого урока на схеме User, включите --feature intercept и зарегистрируйте траверзер DeletedAtIsNil. Затем проверьте три пути: client.User.Query().All(ctx) не вернёт веру; client.User.Get(ctx, вера-id) вернёт NotFoundError; client.User.Query().QueryPosts().CountX(ctx) посчитает на один пост меньше, чем client.Post.Query().CountX(ctx) — разница и есть пост «удалённого» автора. Наконец, добавьте intercept.Func с лимитом и убедитесь по логу драйвера, что SQL получил LIMIT 1000 только там, где клиент не задал свой.
Соберите траверзер живых строк
Впишите типизированный траверзер, тип билдера и метод наложения предиката.
Путь запроса через конвейер чтения
Соберите жизнь выборки от вызова хендлера до строк из базы.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Что дальше#
Вы умеете перехватывать чтение: ent.Interceptor и ent.Querier как конвейер с обязательным next.Query, три уровня регистрации — схема, тип, весь граф, фича-флаг intercept и пакет intercept с intercept.Func и траверзерами, мягкое удаление, ставшее автоматическим, и карта дрейфа запросов, на которой видно, какой фильтр где доезжает. Симметрия с семнадцатым уроком полная: хуки — мутации, интерсепторы — чтение. Но и траверзер, и интерсептор фильтруют, не решая: строку можно спрятать из ленты, при этом мутация остаётся доступной любому контексту без viewer-а. Дальше — privacy-политики девятнадцатого урока: ent.Policy с EvalQuery и EvalMutation, решения privacy.Allow, privacy.Deny и privacy.Skip, deny по умолчанию и тесты политики. Домашнее задание ниже превратит ручные предикаты четырнадцатого урока в схемный контракт. Полный план из 25 уроков — на странице курса, мягкое удаление руками — в четырнадцатом уроке, хуки и hook.Reject — в семнадцатом, With-загрузки — в одиннадцатом, предикаты Where — в десятом.
Карточки: интерсепторы
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- ent interceptors — middleware для запросов чтения: ent.Interceptor = Intercept(next ent.Querier) ent.Querier, адаптер ent.InterceptFunc; ent.Querier.Query(ctx, ent.Query) (ent.Value, error); next.Query — обязательный хвост, как next.Mutate у хуков.
- Регистрация: client.Intercept — весь граф, client.User.Intercept — один тип, func (User) Interceptors() []ent.Interceptor — схема (исполняется раньше рантайма, требует import _ ent/runtime).
- Фича-флаг --feature intercept включает сгенерированный пакет intercept: intercept.Func для generic-правок, intercept.TraverseFunc/TraverseUser для траверзеров, ent.QueryFromContext для проверки заданного Limit.
- Интерсептор исполняется на финальной стадии запроса (до SQL и после строк), траверзер — на каждом шаге дрейфа запроса: фильтры по умолчанию — траверзеры, логи и кеш — интерсепторы.
- Мягкое удаление из четырнадцатого урока автоматизируется одним траверзером q.Where(user.DeletedAtIsNil()) в схеме: Get по «удалённому» вернёт NotFoundError, Count посчитает только живых.
- Типовые кейсы: лимит по умолчанию (intercept.Func + QueryFromContext), лог чтения с %T по типу билдера, фильтры тенантов в обходах.
- Границы: интерсепторы не применяются к мутациям (хуки), не решают доступ (privacy исполняется раньше) и не фильтруют соседние типы — траверзер User не режет client.Post.Query().
- Count и Select идут через конвейер с интерсепторами; GroupBy и Modify проверяйте на своей версии — исторически там бывали слепые пятна.
Домашнее задание
Работайте на базе из восьмого урока — анна, борис, вера, их посты и группа go-ninja — с включённым логом SQL драйвера. Первое: включите --feature intercept, запустите go generate и убедитесь, что в ent появился пакет intercept; запишите, какие файлы он добавил. Второе: соберите траверзер живых строк q.Where(user.DeletedAtIsNil()) в Interceptors() схемы User, «удалите» веру через SetDeletedAt из четырнадцатого урока и докажите три пути: All не вернёт веру, Get по её id вернёт NotFoundError, QueryPosts().CountX вернёт на один пост меньше, чем Post.Query().CountX. Третье: зарегистрируйте тот же фильтр временно через client.User.Intercept и через client.Intercept (с type assertion *ent.UserQuery) — сравните, как ведут себя выборки групп go-ninja и постов; зафиксируйте в комменте, почему схемная регистрация надёжнее. Четвёртое: соберите intercept.Func с лимитом по умолчанию 1000 через ent.QueryFromContext и по логу драйвера докажите, что LIMIT появился только на запросе без явного Limit, а выборка с Limit(3) из двенадцатого урока не пострадала. Пятое: напишите симметричный траверзер Post с фильтром живых авторов — и проверьте, что client.Post.Query().CountX(ctx) и обход QueryPosts() теперь согласованы. Вопрос на подумать: почему фильтр мягкого удаления обязан жить в схеме, а лимит по умолчанию — можно оставить в client.Intercept, и что сломается, если поменять их местами?
Личные заметкиПусто
Вопросы и ответы по уроку
Чем интерсептор отличается от хука и когда что использовать?
Хук — перехват мутаций: ent.Hook оборачивает ent.Mutator и исполняется до записи; интерсептор — перехват запросов чтения: ent.Interceptor оборачивает ent.Querier и исполняется на финальной стадии выборки. Практическое правило: правите поля, валидируете, аудитите или запрещаете запись — хук; фильтруете выборки, ставите лимиты, логируете чтение — интерсептор. Мягкое удаление требует обоих: хук превращает Delete в SetDeletedAt, интерсептор прячет помеченные строки.
Почему после включения Interceptors() в схеме падает ошибка про ent/runtime?
Схемные интерсепторы, как и схемные хуки, вшиваются в клиент при его создании через сгенерированный ent/runtime.go. Если пакет runtime не импортирован, инициализация не случится, и ent вернёт ошибку вида «uninitialized ... (forgotten import ent/runtime?)». Лечение — пустой импорт в main: import _ «ваш-модуль/ent/runtime». Это же правило действует для Policy() из девятнадцатого урока.
Применяются ли интерсепторы к Count, Exist и With-загрузкам?
Count и Exist исполняются тем же конвейером чтения и проходят через интерсепторы — фильтр живых строк влияет на счётчики так же, как на All. With-загрузки исполняются через клиент целевого типа, поэтому траверзер User фильтрует и данные, подгруженные с постов. Проверяйте на своей версии только экзотику: GroupBy и кастомные Modify-пути исторически были слепыми пятнами интерсепторов.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
go-kit: микросервисный тулкит
Курс go-kit: endpoint, транспорт, логирование, метрики, трейсинг, circuit breaker, service discovery. 25 уроков с итоговым микросервисом.
- Понимать архитектуру endpoint → transport → service
- Оборачивать сервисы middleware: логи, метрики, трейсинг
- Подключать HTTP и gRPC транспорты
- Ставить rate limiting и circuit breaker