---
title: "Interceptors"
description: "ent interceptors: InterceptFunc и ent.Querier, фича-флаг intercept, траверзеры, мягкое удаление через интерсептор и дрейф запросов по графу."
course: "ent: графовая ORM с кодогенерацией"
url: "https://golangmaster.ru/courses/ent/lessons/18-interceptors/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Interceptors

*Курс «ent: графовая ORM с кодогенерацией», урок 18 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/ent/lessons/18-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.

Маршрут урока — от первой обёртки до фильтра, который работает сам. Сначала — сигнатура: 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?

- ✅ На финальной стадии: при исполнении запроса — до SQL и после получения строк
- После запроса: строки уже в памяти, интерсептор их дорабатывает
- В момент цепочки Where — сразу при сборке билдера
- На этапе генерации: интерсептор вшивается в код go generate

Интерсептор — middleware финальной стадии чтения: запрос собран билдером, проверен политикой, и перед исполнением стейтмента конвейер прогоняет его через цепочку интерсепторов. Поэтому правка билдера (Where, Limit) ещё влияет на SQL, а полученные строки можно дописать после next.Query. На SetX и сборке билдера интерсепторы не подписаны — там живут мутации и билдеры.

### Три уровня регистрации

Регистрация повторяет карту хуков из семнадцатого урока, и тот же принцип «схема — место политик, рантайм — место настроек» работает здесь без поправок. 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) для «удалённой» веры?

- ✅ NotFoundError: фильтр вшит в выборку, строка не видна
- Строку веры: Get идёт по первичному ключу и игнорирует интерсепторы
- Пустую сущность без ошибки: Get возвращает nil-обёртку
- Панику: интерсепторы не работают на точечных запросах

Get(ctx, id) — это Query().Where(IDEQ(id)).Only(ctx): тот же конвейер, тот же интерсептор. Предикат DeletedAtIsNil попадает в SQL, строка веры не находится, Only отвечает NotFoundError — ловите её через ent.IsNotFound из девятого урока. Именно это поведение делает мягкое удаление честным: нет пути чтения, который бы обошёл фильтр, — в том числе через первичный ключ.

### Дрейф запросов: где крепится фильтр

Запрос в приложении дрейфует: хендлер собирает билдер, на него насаживаются предикаты, сортировки и 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 до и после траверзера

```go
// анна — 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
```

**Что выведет программа?**

- ✅ A = 2, B = 4: траверзер User режет пользователей, но не сами посты
- A = 2, B = 3: траверзер User вычищает и посты удалённых
- A = 3, B = 4: траверзер не влияет на Count, только на All
- A = 0, B = 0: траверзер ломает запросы без предикатов клиента

Траверзер User исполняется на выборках User — и CountX по пользователям честно вернёт 2 (живых анна и борис). Но client.Post.Query() — выборка Post: она не проходит через траверзер User, поэтому 4 поста на месте. Пост веры исчезнет только из обходов через User (QueryPosts) — как в предыдущем примере. Нужен фильтр на самих постах — пишите траверзер Post: симметрия типов здесь дословная.

### Границы: что интерсептор не делает

Четыре границы честности. Первая: интерсепторы применяются только к чтению — на мутации они не подписаны, там территория хуков из семнадцатого урока; 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)?

- ✅ Интерсептор не сработает: он применяется только к чтению
- Удаление отменится: интерсептор запретит удалять строки
- Удаление исполнится, а интерсептор отфильтрует результат
- Интерсептор перепишет DELETE на UPDATE с SetDeletedAt

Интерсепторы — middleware пути чтения: DeleteOneID исполняет мутацию, её перехватывают хуки из семнадцатого урока. Хотите запретить жёсткое удаление — hook.Reject(ent.OpDelete, ent.OpDeleteOne) из семнадцатого; хотите превратить его в мягкое — хук с m.SetField("deleted_at", time.Now()). Интерсептор на этот путь влияния не имеет — он лишь делает «удалённые» строки невидимыми для выборок.

### Чеклист интерсепторов

- 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 только там, где клиент не задал свой.

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

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

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

Подсказка: Первый пропуск — типизированный траверзер из пакета intercept (фича-флаг intercept); второй — конкретный тип запроса, который приходит вместо generic ent.Query; третий — метод билдера, принимающий предикат из десятого урока.

Правильные ответы в пропусках: `TraverseUser`, `UserQuery`, `Where`

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

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

Правильный порядок строк:

1. `хендлер собирает client.User.Query().Where(user.EmailEQ(...)) — билдер ещё пустой каркас`
2. `запрос дрейфует: насаживаются Order из двенадцатого и With-загрузки из одиннадцатого`
3. `политика privacy проверена первой: EvalQuery разрешил или отказал (девятнадцатый урок)`
4. `интерсепторы схемы Interceptors() — внешний слой: DeletedAtIsNil вшит в Where`
5. `рантайм-интерсепторы: лимит по умолчанию из QueryFromContext подставлен`
6. `next.Query доходит до sqlgraph: SELECT с готовым WHERE уходит в базу, строки возвращаются`

### Что дальше

Вы умеете перехватывать чтение: ent.Interceptor и ent.Querier как конвейер с обязательным next.Query, три уровня регистрации — схема, тип, весь граф, фича-флаг intercept и пакет intercept с intercept.Func и траверзерами, мягкое удаление, ставшее автоматическим, и карта дрейфа запросов, на которой видно, какой фильтр где доезжает. Симметрия с семнадцатым уроком полная: хуки — мутации, интерсепторы — чтение. Но и траверзер, и интерсептор фильтруют, не решая: строку можно спрятать из ленты, при этом мутация остаётся доступной любому контексту без viewer-а. Дальше — privacy-политики девятнадцатого урока: ent.Policy с EvalQuery и EvalMutation, решения privacy.Allow, privacy.Deny и privacy.Skip, deny по умолчанию и тесты политики. Домашнее задание ниже превратит ручные предикаты четырнадцатого урока в схемный контракт. Полный план из 25 уроков — на [странице курса](/courses/ent/), мягкое удаление руками — в [четырнадцатом](/courses/ent/lessons/14-delete/) уроке, хуки и hook.Reject — в [семнадцатом](/courses/ent/lessons/17-huki/), With-загрузки — в [одиннадцатом](/courses/ent/lessons/11-with/), предикаты Where — в [десятом](/courses/ent/lessons/10-predikaty/).

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

- **Сигнатура ent.Interceptor** — Интерфейс с одним методом Intercept(next ent.Querier) ent.Querier; ent.Querier — Query(ctx, ent.Query) (ent.Value, error). Адаптеры: ent.InterceptFunc и ent.QuerierFunc. next.Query — обязательный хвост: без него выборка умирает тихо. Интерсептор — middleware пути чтения.
- **Три уровня регистрации** — client.Intercept — весь граф (рантайм), client.User.Intercept — один тип, func (User) Interceptors() []ent.Interceptor — схема: встраивается в код и работает у всех клиентов. Схемные исполняются раньше рантайм-; для схемных нужен import _ ent/runtime.
- **Фича-флаг intercept** — generate --feature intercept приносит пакет intercept: intercept.Func — generic-интерсептор (q intercept.Query без type assertion), intercept.TraverseFunc — траверзер на каждый шаг обхода графа, intercept.TraverseUser — типизированная версия с *ent.UserQuery, ent.QueryFromContext — снапшот запроса для проверки Limit.
- **Интерсептор против траверзера** — Интерсептор исполняется один раз на финальной стадии: правит запрос перед исполнением и строки после. Траверзер — на каждом шаге обхода графа: правит промежуточные и финальные запросы до склейки. Фильтры по умолчанию — траверзеры; логирование и кеш — интерсепторы.
- **Мягкое удаление через интерсептор** — Траверзер q.Where(user.DeletedAtIsNil()) в Interceptors() схемы вшивает фильтр живых во все выборки User: All, First, Only, Get по id, Count, Exist и обходы рёбер. Get по «удалённому» — NotFoundError. Ручной Where из четырнадцатого урока больше не нужен — и не забудется.
- **Границы интерсепторов** — Только чтение: мутации — хуки (ent.Op, next.Mutate). Фильтр, а не решение: право «можно ли» у privacy-политик, которые исполняются раньше. Фильтр User не режет client.Post.Query() — нужен траверзер Post. Агрегатные пути (GroupBy, Modify) проверяйте на своей версии.

## Коротко: что запомнить

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