Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 18 из 25 ~18 мин +50 XP

Interceptors

Семнадцатый урок закрыл половину мира: ent хуки перехватывают мутации, но на запросы чтения они не подписаны. Сегодня — ent interceptors: интерсепторы, middleware для всех типов запросов ent. Разберём интерфейс ent.Interceptor и адаптер ent.InterceptFunc, внутренности ent.Querier и generic-запрос ent.Query, включим фича-флаг intercept и познакомимся со сгенерированным пакетом intercept — intercept.Func и траверзерами intercept.TraverseFunc. Главный сюжет прикладной: мягкое удаление из четырнадцатого урока наконец становится автоматическим — предикат DeletedAtIsNil вшивается во все выборки схемы одним интерсептором, и забытый Where больше не пропускает «удалённых» в ленту. Отдельно — дрейф запросов: почему фильтр, вшитый не на том этапе исполнения, доезжает не везде, чем интерсептор отличается от траверзера и где живёт лимит по умолчанию. По канону курса: анна, борис, вера, группа go-ninja и счётчик posts_count.

Содержание урока · 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() из семнадцатого урока, встраивается в сгенерированный код и работает у всех клиентов без исключений.

Первый интерсептор: лог чтения

GO
client.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.QuerierQuery(ctx, ent.Query) (ent.Value, error) — следующий шагnext.Query — обязательный хвост каждого интерсептора
ent.QuerierFuncадаптер тела в Querierобёртка логики внутри InterceptFunc
ent.Querygeneric-интерфейс билдера запросаконкретика через 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

GO
client.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): у интерсепторов там исторически бывали слепые пятна.

Соберите траверзер живых строк

Впишите типизированный траверзер, тип билдера и метод наложения предиката.

client.User.Intercept( intercept.(func(ctx context.Context, q *ent.) error { q.(user.DeletedAtIsNil()) return nil }), )

Путь запроса через конвейер чтения

Соберите жизнь выборки от вызова хендлера до строк из базы.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Что дальше#

Вы умеете перехватывать чтение: 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 — в десятом.

Карточки: интерсепторы

Термин

Сигнатура ent.Interceptor

нажмите, чтобы перевернуть
Определение

Интерфейс с одним методом Intercept(next ent.Querier) ent.Querier; ent.Querier — Query(ctx, ent.Query) (ent.Value, error). Адаптеры: ent.InterceptFunc и ent.QuerierFunc. next.Query — обязательный хвост: без него выборка умирает тихо. Интерсептор — middleware пути чтения.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 · Interceptors)

Вопросы и ответы по уроку

Чем интерсептор отличается от хука и когда что использовать?

Хук — перехват мутаций: 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-kit25 уроков~14 чпродвинутый25 уроков уже доступны

go-kit: микросервисный тулкит

Курс go-kit: endpoint, транспорт, логирование, метрики, трейсинг, circuit breaker, service discovery. 25 уроков с итоговым микросервисом.

  • Понимать архитектуру endpoint → transport → service
  • Оборачивать сервисы middleware: логи, метрики, трейсинг
  • Подключать HTTP и gRPC транспорты
  • Ставить rate limiting и circuit breaker