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

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

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

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

Предикаты и фильтры

В девятом уроке мы научились исполнять запросы и прошли каталог where.go — но применяли предикаты по одному, максимум складывали в срез за ветвлениями if. Сегодня — ent предикаты как самостоятельный инструмент. Сначала тип: почему predicate.User — это функция, а не структура, и что из этого следует для вашего кода. Затем три комбинатора — user.And, user.Or, user.Not — и честный разбор того, как ent расставляет скобки между AND и OR, чтобы приоритет никогда не сыграл против вас. Дальше прикладная часть: фабрики предикатов — как оформлять условия функциями, чтобы переиспользовать их между запросами, как скопы в GORM. Отдельные сюжеты — ловушка NOT на Optional-полях с NULL и семейство HasEdge: фильтры по рёбрам, от HasGroups() до вложенных HasGroupsWith. После этого урока сколь угодно сложный фильтр будет собираться у вас из компилируемых деталей.

Содержание урока · 11 разделов

План урока повторяет путь одного условия: от значения-функции до фрагмента SQL. Девятый урок оставил нам хвост: предикаты умеют складываться в срез, но комбинаторы упоминались там одной строкой таблицы. Сегодня разбираем ent предикаты системно. Первый сюжет — тип predicate.User и предикаты как значения: что именно возвращает user.AgeGTE(18) и почему это открывает дорогу фабрикам. Второй — комбинаторы And, Or, Not: их сигнатуры в where.go и механика внутри dialect/sql. Третий — приоритет AND и OR и скобки, которые ent ставит сам. Четвёртый — фабрики предикатов и переиспользование условий между запросами, ручкой-к-ручке со скопами GORM. Пятый — ловушка NOT на Optional-полях, где в игру входит трёхзначная логика SQL. Шестой — HasEdge-предикаты по рёбрам, которые завязывают WHERE на соседние таблицы. Данные те же, что и весь курс: анна, борис и вера, группы и посты, база из восьмого урока.

ent предикаты: условия как значения#

Откроем сгенерированный каталог blog/ent/predicate — там нет ничего, кроме объявлений типов, по одному на каждую схему проекта: type User func(*sql.Selector). Это весь секрет. Предикат в ent — не строка с SQL, не структура с флажками и не AST, а обычная функция Go, которая принимает селектор запроса и дописывает ему условие. user.AgeGTE(18) — не «вызов проверки возраста», а мгновенное создание замыкания: возвращённая функция ещё ничего не сделала, она станет условием только тогда, когда её передадут в Where и билдер начнёт собирать стейтмент. Отсюда три практических следствия. Первое: предикаты — полноценные значения, их можно класть в переменные и срезы, передавать в функции и возвращать из функций. Второе: предикаты типизированы по схеме — predicate.User нельзя подставить в запрос групп, компилятор остановит вас до базы. Третье: предикаты иммутабельны — функция ничего не меняет, поэтому одно и то же условие можно безопасно шарить между горутинами и запросами. Если вы гуглили predicates ent, то попали по адресу: страница документации Predicates описывает ровно эти функции — полевые, рёберные и три комбинатора, которым посвящён урок.

Предикат в руке: переменная, срез, ветвление

GO
// предикат — значение: можно держать в переменной:
adult := user.AgeGTE(18)

// ...и в типизированный срез из пакета predicate:
preds := []predicate.User{adult, user.HasPosts()}

// ...и дописывать по ходу дела:
if q != "" {
        preds = append(preds, user.NameContainsFold(q)) // поиск без регистра
}

// Where принимает любое количество предикатов —
// срез раскрывается многоточием:
users, err := client.User.Query().Where(preds...).All(ctx)

Тип среза — []predicate.User, импорт — blog/ent/predicate. Обратите внимание: взятие adult из переменной и повторный вызов user.AgeGTE(18) неотличимы для билдера — замыкания равнозначны. Где собирать срез — на месте или в отдельной функции — вопрос архитектуры, и следующий сюжет фабрик показывает самый выгодный вариант. Никаких строк-условий здесь нет и быть не может: опечатка в имени предиката — ошибка компиляции.

Комбинаторы And, Or, Not в where.go#

Для каждой схемы where.go выпускает три функции-комбинатора, и они сами являются предикатами — возвращают predicate.User, поэтому вкладываются друг в друга без ограничений. Сигнатуры стоит запомнить дословно: And(predicates ...predicate.User) predicate.User, Or(predicates ...predicate.User) predicate.User и Not(p predicate.User) predicate.User. У And и Or аргументы вариативные — можно соединить хоть пять условий разом; у Not аргумент строго один: хотите отрицать несколько условий — сначала соберите их user.And, потом отрицайте. Под капотом все три делегируют в пакет dialect/sql: sql.AndPredicates, sql.OrPredicates и sql.NotPredicates. Механика там изящная: на время работы комбинатора селектор переводится в режим сбора — внутренние предикаты не смешиваются с внешним WHERE, а копятся в отдельный список. Дальше switch по длине: ноль предикатов — ничего не добавляется; один — он проходит прозрачно, без обёртки и без оператора; больше одного — все соединяются And(...) или Or(...) и отдаются общему Where. Из «один — прозрачная обёртка» следует забавный факт: user.Or(p) с единственным аргументом равносилен самому p.

Три комбинатора в работе

GO
// два Where и один user.And — одинаковый SQL:
q1 := client.User.Query().
        Where(user.AgeGTE(18)).
        Where(user.RoleEQ(user.RoleAdmin))

q2 := client.User.Query().
        Where(user.And(user.AgeGTE(18), user.RoleEQ(user.RoleAdmin)))
// оба: WHERE age >= 18 AND role = 'admin'

// OR: «админ или уже автор постов»:
q3 := client.User.Query().
        Where(user.Or(
                user.RoleEQ(user.RoleAdmin),
                user.HasPosts(), // рёберный предикат: EXISTS
        ))

// NOT: «все, кроме админов»:
q4 := client.User.Query().
        Where(user.Not(user.RoleEQ(user.RoleAdmin)))
// WHERE NOT (role = 'admin')

q1 и q2 дают один и тот же WHERE: несколько Where соединяются через AND, а user.And просто собирает те же условия одним значением — разница только в стиле. Комбинаторы вкладываются: user.Not(user.Or(...)), user.Or(user.And(...), ...) — глубина любая. Импорт комбинаторов — тот же пакет user, что и полевых предикатов: отдельного пакета And/Or в ent нет.

Проверка: AND без лишних букв

Запрос: Query().Where(user.NameEQ("anna")).Where(user.AgeGTE(18)).All(ctx). Какой вызов даст ровно тот же самый WHERE?

Приоритет AND и OR: скобки, которые ставит ent#

В чистом SQL AND связывает сильнее OR: условие a AND b OR c читается как (a AND b) OR c, и забывчивые скобки ломали данные чаще, чем опечатки. ent не оставляет этот вопрос на вашу память — скобки расставляет генерируемый слой. Внутри dialect/sql у предиката есть счётчик глубины: когда And или Or собирают свои операнды, каждый вложенный композит получает глубину на единицу больше. Функция mayWrap смотрит на неё при печати: группа на верхнем уровне обходится без скобок — WHERE a AND b AND c, а группа, попавшая внутрь другой, печатается в скобках. Отсюда два правила, которые покрывают всю практику. Правило первое: Where(user.AgeGTE(18)).Where(user.Or(...)) даёт age >= 18 AND (role = 'admin' OR ... ) — OR-группа берётся в скобки автоматически, и смысл совпадает с тем, как записан Go-код. Правило второе: user.Or(user.And(a, b), c) даёт (a AND b) OR c — конъюнкция внутри дизъюнкции тоже в скобках. Итог: при смешивании Where, user.And и user.Or приоритет AND/OR всегда явный — ent компилирует структуру Go-выражения в структуру WHERE один в один, и читать сгенерированный SQL так же легко, как код.

Скобки на практике

GO
// OR-группа внутри запроса с другими условиями:
users, err := client.User.Query().
        Where(user.AgeGTE(18)).            // условие-1
        Where(user.Or(                     // условие-2: OR-группа
                user.RoleEQ(user.RoleAdmin),
                user.HasPosts(),
        )).
        All(ctx)
// WHERE age >= 18 AND (role = 'admin' OR EXISTS (...))
//                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ скобки поставил ent

// And внутри Or: конъюнкция тоже в скобках:
pred := user.Or(
        user.And(user.AgeGTE(18), user.HasPosts()),
        user.RoleEQ(user.RoleAdmin),
)
// (age >= 18 AND EXISTS (...)) OR role = 'admin'

Верхнеуровневый user.Or сам по себе скобок не получил бы — операнды-композиты в mayWrap печатаются обёрнутыми, как только группа оказывается внутри другой группы. Именно поэтому комбинация «обычные Where плюс одна Or-группа» — самый частый и самый безопасный паттерн фильтрации: обязательные условия через Where, ветвление — одной Or.

Какой SQL уйдёт в базу

Прочитайте код и выберите, что он выведет:

// анна — админ; борис — нет; оба взрослые:
users, err := client.User.Query().
        Where(user.AgeGTE(18)).
        Where(user.Or(
                user.RoleEQ(user.RoleAdmin),
                user.NameEQ("boris"),
        )).
        All(ctx)

// какой WHERE построит ent?

Фабрики предикатов: переиспользование условий#

Как только предикат стал значением, появляется и правильный дом для условий — функции, которые их производят. Фабрика предикатов — это функция, возвращающая predicate.User: func Adults(min int) predicate.User { return user.AgeGTE(min) }. Зачем она нужна, если можно написать user.AgeGTE(18) на месте? Первое — имя на языке домена: Staff() и Authors() читаются в обработчике лучше, чем цепочка RoleEQ и HasPosts. Второе — параметризация: одна фабрика Adults(min) покрывает и «совершеннолетние», и «старше тридцати» — глобальная переменная так не умеет. Третье — композиция: фабрики вкладываются друг в друга, Staff() — это user.And(RoleEQ админа, Authors()). Четвёртое — единая точка правки: поменялась бизнес-логика «активного пользователя» — меняете одну функцию, а не десять запросов. Всё это вам уже знакомо по курсу GORM: скопы из девятнадцатого урока решали ту же задачу — переиспользование кусков WHERE между запросами, — просто там фабрика обёртывала *gorm.DB, а здесь возвращает чистую функцию. Предикаты иммутабельны, поэтому фабрики безопасно звать из любых горутин; живут они обычно в отдельном пакете predicates рядом со схемами. Единственное правило: фабрика не исполняет запрос и не держит контекста — только собирает значение-условие, исполнение по-прежнему в All(ctx).

Пакет фабрик для схемы User

GO
// predicates/predicates.go — фабрики предикатов проекта:

// Adults — пользователи от min лет:
func Adults(min int) predicate.User {
        return user.AgeGTE(min)
}

// Authors — уже написали хотя бы один пост:
func Authors() predicate.User {
        return user.HasPosts()
}

// Staff — админы среди авторов: фабрика из фабрик:
func Staff() predicate.User {
        return user.And(user.RoleEQ(user.RoleAdmin), Authors())
}

// использование — как обычного предиката:
users, err := client.User.Query().
        Where(Staff(), Adults(18)).
        All(ctx)

Where принимает фабрики рядом с инлайн-предикатами — тип один и тот же, predicate.User. Параметр min вместо магической константы делает фабрику пригодной для разных сценариев, а Staff() показывает композицию: комбинаторы позволяют собирать доменные понятия из доменных же деталей. В SQL это развернётся в age >= 18 AND role = 'admin' AND EXISTS (...) — три конъюнкции без скобок.

Проверка: зачем фабрика

Чем фабрика func Adults(min int) predicate.User лучше готовой глобальной переменной var Adults = user.AgeGTE(18)?

Сколько строк вернёт фильтр

Прочитайте код и выберите, что он выведет:

// таблица: анна (26, админ, 2 поста), борис (19, юзер, 0 постов),
// вера (17, юзер, 0 постов, bio = NULL):

users, err := client.User.Query().
        Where(
                user.Or(user.AgeGTE(18), user.BioIsNil()),
                user.Not(user.HasPosts()),
        ).
        All(ctx)

fmt.Println(len(users))

Not и NULL: трёхзначная ловушка#

SQL — трёхзначная логика: сравнение возвращает TRUE, FALSE или UNKNOWN, и UNKNOWN случается всякий раз, когда в сравнении участвует NULL. Секция WHERE оставляет только строки с TRUE. Теперь подставьте NOT: для строки с bio = NULL сравнение bio = 'go-энтузиаст' даёт UNKNOWN, а NOT UNKNOWN — всё тот же UNKNOWN. Строка выпадает из выборки, хотя «не равняется» она вроде бы всем. Это та же ловушка, что у BioNEQ и BioNotIn: любой отрицательный фильтр на Optional-поле молча теряет NULL-строки — и не ищите тут багу ent, это семантика SQL. Лекарство — явная NULL-ветка: user.Or(user.BioIsNil(), user.Not(user.BioEQ("go-энтузиаст"))) собирает «пустые отдельно, не равные отдельно» и возвращает всех, кого вы имели в виду. Вторая половина сюжета — классическое противопоставление NOT IN и NOT EXISTS из ручного SQL: подзапрос, вернувший NULL, превращает весь NOT IN в пустой результат. ent закрыл эту дыру конструктивно: рёберные подзапросы строятся по первичным ключам с NOT NULL, поэтому user.Not(user.HasGroups()) катастрофы NULL-списка устроить не может. Разделяйте: Not поверх рёберных Has* — безопасна, Not и NotIn поверх Optional-полей — требует IsNil-ветки.

Отрицательный фильтр без потери строк

GO
// данные: анна bio = 'go-энтузиаст', борис bio = 'sql', вера bio = NULL

// НАИВНО: «все, чьё био не про go» — вера потеряется:
pred := user.Not(user.BioEQ("go-энтузиаст"))
// NOT (bio = 'go-энтузиаст') по NULL даёт UNKNOWN: строка мимо

// ПРАВИЛЬНО: пустые — отдельной веткой:
pred = user.Or(
        user.BioIsNil(),
        user.Not(user.BioEQ("go-энтузиаст")),
)
// WHERE bio IS NULL OR NOT (bio = 'go-энтузиаст')

// множество — та же история: BioNotIn теряет NULL-строки:
user.BioNotIn("go-энтузиаст", "sql") // веры в выборке не будет

Форма «Or(IsNil, Not(...))» — идиома для Optional-полей: запомните её как пару к трёхзначной логике. Для обязательных полей вроде name или age этой ловушки нет: NULL там невозможен по схеме, и NameNEQ работает без сюрпризов. NOT IN с подзапросом в рукописном SQL опаснее всего — в ent это место закрыто рёберными предикатами, которые собирают подзапросы по NOT NULL ключам.

Проверка: сколько потеряли

У анны bio = 'go', у бориса bio = 'sql', у веры bio = NULL. Сколько строк вернёт Query().Where(user.Not(user.BioEQ("go"))).All(ctx)?

HasEdge-предикаты: фильтры по рёбрам#

Самое ценное семейство предикатов фильтрует не поля, а связи. В документации оно называется HasEdge и HasEdgeWith: для ребра owner вы пишете pet.HasOwner(), а с условием на соседа — pet.HasOwnerWith(user.Name("a8m")). Для нашей схемы User where.go выпускает HasPosts, HasGroups, HasFriends и пары с With на каждое ребро. Сигнатура второй формы — HasGroupsWith(preds ...predicate.Group): предикаты соседа типизированы его схемой, и внутри они применяются к селектору подзапроса — это sqlgraph.HasNeighborsWith, который принимает func(sql.Selector). В девятом уроке мы называли все эти формы EXISTS-подзапросами — заглянем теперь глубже. Рёбра разных форм компилируются по-разному: o2m-ребро вроде постов даёт EXISTS с коррелированным сравнением ключей, а m2m через таблицу связи — id IN (SELECT ... FROM group_users JOIN groups ...): сначала ограничиваем таблицу-прослойку, потом соседнюю. Вложенность не ограничена: user.HasGroupsWith(group.HasUsersWith(user.NameEQ("boris"))) — «состоит в группе, где есть борис». И важная договорённость напоследок: эти предикаты только фильтруют. Найденные соседи сами не приезжают — их загрузит жадная загрузка With из одиннадцатого урока 11-with.

HasEdge от простого к вложенному

GO
// состоит хотя бы в одной группе:
users, _ := client.User.Query().Where(user.HasGroups()).All(ctx)

// состоит в конкретной группе — условие на соседа:
users, _ = client.User.Query().
        Where(user.HasGroupsWith(group.NameEQ("go-ninja"))).
        All(ctx)

// есть друг-совершеннолетний (само-ребро friends):
users, _ = client.User.Query().
        Where(user.HasFriendsWith(user.AgeGTE(18))).
        All(ctx)

// вложенность: в группе, где состоит boris:
users, _ = client.User.Query().
        Where(user.HasGroupsWith(group.HasUsersWith(user.NameEQ("boris")))).
        All(ctx)
// m2m: WHERE id IN (SELECT user_id FROM group_users
//                   JOIN groups ON ... WHERE ...)

Все четыре формы — обычные predicate.User, поэтому свободно комбинируются с And/Or/Not и фабриками. Предикат соседа применяется к селектору подзапроса: group.NameEQ попадает в WHERE внутренней выборки. Комбинаторы на соседях работают так же, как на корне — вложенное HasUsersWith может содержать и Or, и Not, и следующий уровень рёбер.

Комбинаторы и рёберные предикаты: сводка#

ИнструментСигнатураSQL-результатКогда брать
user.AndAnd(predicates ...predicate.User) predicate.Usera AND b — без скобок на верхнем уровнесобрать обязательные условия одним значением
user.OrOr(predicates ...predicate.User) predicate.User(a OR b) — вложенная группа в скобкахветвление «или то, или это»
user.NotNot(p predicate.User) predicate.UserNOT (p) — ровно один аргументотрицание сложного условия; для полей есть NEQ/NotIn
HasPosts, HasGroupsfunc() predicate.UserEXISTS или IN-подзапрос по таблице связи«есть хотя бы один сосед»
HasGroupsWithfunc(preds ...predicate.Group) predicate.Userподзапрос с условием на соседафильтр по свойствам соседей
фабрикаfunc UsersActive() predicate.Userна SQL не влияет — только собирает условиепереиспользование и композиция фильтров

Соберите фильтр из комбинаторов

Впишите дизъюнкцию, рёберный предикат с условием на группу, отрицание и NULL-проверку Optional-поля.

// 1. совершеннолетние админы, ИЛИ все, кто состоит в go-ninja: pred := user.( user.AgeGTE(18), user.RoleEQ(user.RoleAdmin), user.(group.NameEQ("go-ninja")), ) // 2. не админ, ИЛИ у пользователя пустое bio — без потери NULL-строк: noAdmin := user.Or( user.(user.RoleEQ(user.RoleAdmin)), user.Bio(), ) users, err := client.User.Query().Where(pred, noAdmin).All(ctx)

Путь одного условия

Соберите жизненный цикл предиката от фабрики до SQL: что происходит и в каком порядке, когда Where получает Or-группу.

Строки

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

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

Что дальше#

Вы умеете собирать фильтры любой сложности, но помните договорённость: рёберные предикаты только отбирают строки, поля рёбер в сущностях после запроса пусты. Их заполнит жадная загрузка — WithGroups, WithPosts и родня из одиннадцатого урока 11-with, который закрывает второе белое пятно после десятого. Домашнее задание делает переиспользование условий пальцевой памятью: соберите пакет фабрик на базе из восьмого урока и проверьте каждую комбинацию против лога SQL. Если захотите освежить контракты исполнения — All, Only, First и служебные формы — они всё в девятом уроке. Полный план из 25 уроков — на странице курса.

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

Термин

predicate.User — что это

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

Функция: в blog/ent/predicate объявлено type User func(*sql.Selector). Предикат — значение: переменная, срез []predicate.User, аргумент и результат функции. Типизирован по схеме — в запрос групп не подставишь. Иммутабелен и потокобезопасен. Ищется в документации по запросу predicates ent — страница Predicates.

1 / 6

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

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Предикат в ent — функция: в blog/ent/predicate объявлено type User func(*sql.Selector). user.AgeGTE(18) возвращает замыкание-значение: его можно класть в срез []predicate.User, возвращать из фабрик и передавать между слоями. Предикаты типизированы схемой и иммутабельны; строковых условий в ent нет.
  • Комбинаторы where.go: And(predicates ...predicate.User), Or(predicates ...predicate.User), Not(p predicate.User) — сами возвращают predicate.User. Внутри — sql.AndPredicates, sql.OrPredicates, sql.NotPredicates: предикаты собираются в список, ноль — ничего, один — прозрачно, больше — общий And/Or.
  • Несколько Where и один user.And дают одинаковый SQL: оба пути — конъюнкция одного среза. user.Or с единственным аргументом прозрачна; user.Not отрицает ровно один предикат — для группы условий заверните её в user.And.
  • Приоритет AND/OR ent берёт на себя: счётчик глубины и mayWrap в dialect/sql печатают вложенные группы в скобках. Where(a).Where(user.Or(b, c)) → a AND (b OR c); user.Or(user.And(a, b), c) → (a AND b) OR c. Структура Go-кода переносится в WHERE один в один.
  • Фабрика предикатов — функция, возвращающая predicate.User: параметризация (Adults(min)), имена домена (Staff, Authors), композиция фабрик и единая точка правки. Аналог скопов GORM из девятнадцатого урока; предикаты не исполняют запросы и безопасны для горутин.
  • Not и NULL: сравнение с NULL даёт UNKNOWN, NOT UNKNOWN = UNKNOWN, а WHERE оставляет только TRUE — отрицательные фильтры на Optional-полях (Not, NEQ, NotIn) теряют NULL-строки. Идиома-лечение: user.Or(user.BioIsNil(), user.Not(user.BioEQ(...))).
  • NOT IN с NULL в подзапросе — классическая ловушка ручного SQL; ent закрыл её конструктивно: рёберные подзапросы строятся по первичным ключам NOT NULL, поэтому user.Not(user.HasGroups()) безопасна. Различайте: Has* с Not — можно, поля Optional с Not — только с IsNil-веткой.
  • HasEdge-предикаты: HasPosts/HasGroups/HasFriends без аргументов и пары с With — HasGroupsWith(preds ...predicate.Group). o2m-ребро компилируется в EXISTS, m2m через таблицу связи — в id IN (SELECT ... FROM group_users JOIN groups ...). Вложенность любая: HasGroupsWith(group.HasUsersWith(user.NameEQ("boris"))); найденных соседей грузит With* из урока 11-with.

Домашнее задание

Работайте на базе из восьмого урока с анной, борисом и верой; включите лог SQL драйвера. Первое: создайте пакет predicates и перенесите туда фабрики Adults(min), Authors() и Staff() — соберите запрос, где Where(Staff(), Adults(18)) живёт рядом с инлайн-предикатом, и убедитесь по логу, что все три условия легли в один AND без скобок. Второе: постройте фильтр «взрослые и (админы или авторы)» двумя способами — через Where(user.Or(...)) и через отдельный срез — и сверьте напечатанный SQL: Or-группа должна оказаться в скобках. Третье: воспроизведите трёхзначную ловушку — запросите user.Not(user.BioEQ("go")) и пересчитайте результат руками, затем добавьте IsNil-ветку через user.Or и добейтесь, чтобы вера вернулась в выборку. Четвёртое: напишите HasEdge-цепочку user.HasGroupsWith(group.HasUsersWith(user.AgeGTE(18))) и найдите в логе подзапрос к таблице связи group_users; затем замените условие на name бориса. Пятое: проверьте прозрачность одиночных комбинаторов — user.Or(user.RoleEQ(user.RoleAdmin)) должна дать тот же WHERE, что и предикат без обёртки. Вопрос на подумать: почему фабрика не может принимать ctx и что сломалось бы в архитектуре, если бы могла?

Личные заметкиПусто
Хранятся только в вашем браузере (ent · Предикаты и фильтры)

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

Чем user.And отличается от нескольких вызовов Where?

Ничем, кроме стиля: оба пути дают одну конъюнкцию. Несколько Where дописывают предикаты в один срез билдера, и при сборке они соединяются через AND; user.And делает то же самое одним значением — через sql.AndPredicates собирает внутренние предикаты и отдаёт общий And(...). Практическое правило: фиксированные обязательные условия пишите отдельными Where, а составное условие, которое хочется назвать именем и переиспользовать, — собирайте user.And в фабрике.

Можно ли положиться на приоритет операторов и не думать о скобках?

Можно и нужно — но не потому, что SQL прощает, а потому, что ent скобки ставит сам. В dialect/sql у групп And/Or есть счётчик глубины: верхнеуровневая группа печатается без скобок, вложенная — в скобках. Поэтому Where(user.AgeGTE(18)).Where(user.Or(...)) всегда даёт age >= 18 AND (a OR b), а не разорванную OR-цепочку. Читая лог SQL, вы увидите ровно ту структуру, которую записали в Go.

Почему Not(BioEQ) не возвращает пользователей с пустым bio?

Это трёхзначная логика SQL, а не поведение ent: сравнение bio = 'go' для NULL даёт UNKNOWN, NOT UNKNOWN остаётся UNKNOWN, а секция WHERE пропускает только TRUE. То же делают NEQ и NotIn на Optional-полях. Если «пустые» должны попадать в выборку, добавьте явную ветку: user.Or(user.BioIsNil(), user.Not(user.BioEQ("go"))). Рёберные предикаты этой ловушки не имеют: ent строит их подзапросы по первичным ключам с NOT NULL.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

go-kit25 уроков~14 чпродвинутый25 уроков уже доступны

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

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

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