---
title: "Условия Where и операторы — строковые, структурные, IN, LIKE, диапазоны"
description: "Условия gorm where: строковые и структурные формы, плейсхолдеры против SQL-инъекции, IN, LIKE, ILIKE, диапазоны, Or и Not, AND-склейка нескольких условий."
course: "GORM: ORM-библиотека для Go"
url: "https://golangmaster.ru/courses/gorm/lessons/07-where/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Условия Where и операторы — строковые, структурные, IN, LIKE, диапазоны

*Курс «GORM: ORM-библиотека для Go», урок 7 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/gorm/lessons/07-where/*

Условия gorm where — самая ходовая часть запросов: почти каждый SELECT начинается с фильтрации, и шестой урок дал её лишь в минимальной дозе. Здесь полный арсенал: три формы условия gorm (строка с плейсхолдерами, структура, карта), правило нулевых полей и его лечение вторым аргументом, IN с его поведением на пустом срезе, LIKE и ILIKE, диапазоны и BETWEEN, Or и Not. Заодно — то, о чём GORM предупреждает отдельной страницей: SQL-инъекция начинается там, где строку условия склеили руками вместо того, чтобы передать аргумент.

Шестой урок [научил читать записи](/courses/gorm/lessons/06-read/): First, Take и Find доставали строки, но условия там мелькали только в примерах — одно done = ? и срез ключей. Пора полный арсенал: Where — самый частый метод в живом сервисе, потому что фильтрация есть почти в каждом запросе, и именно здесь сходятся удобство и ответственность. Условия gorm принимают три формы — строковую, структурную и карту — и у каждой свой характер: строковая гибкая и полная, структурная типобезопасная, но слепая к нулям, карта честная ко всему, что вы положили. Одна честная оговорка о формате: СУБД в браузерной песочнице не живёт, поэтому runner-ов в курсе нет — вас проверяют quiz, predict, fill, order и карточки, а примеры рассчитаны на локальный Go с PostgreSQL или SQLite. Рабочие данные — те же пять задач из [пятого урока](/courses/gorm/lessons/05-create/) в таблице todo_items; для наглядности считаем, что статусы уже разъехались: у задач 1 и 5 done = true и статус done, у остальных — todo или doing, приоритеты 0, 0, 2, 3, 1. Каждый пример снабжён SQL-следом — его печатает логгер из второго урока в режиме Info.

### gorm where: строковые условия и плейсхолдеры

Основная форма — строковая: db.Where("status = ?", "todo"). Внутри метода Where(query interface{}, args ...interface{}) из chainable_api.go такая пара уходит в Statement.BuildCondition, который видит строку с вопросительным знаком и заворачивает её в clause.Expr — фрагмент SQL плюс список аргументов. При сборке запроса каждый ? заменяется плейсхолдером диалекта ($1 на PostgreSQL, ? на SQLite и MySQL), а значение едет в драйвер отдельно от текста команды. Оператор — любой SQL-ный: = и <> для неравенства, >, >=, <, <= для сравнений, AND и OR прямо внутри строки. Есть и короткая форма без плейсхолдера: db.Where("status", "doing") — строка без пробелов и с единственным аргументом превращается в равенство status = 'doing'. Строка условия — это сырой SQL: GORM не разбирает её на части, он лишь подставляет значения вместо ?. Отсюда два следствия: свобода (любой оператор и выражение, которые поймёт база) и ответственность — с неё и начнём.

Страница Security в документации GORM формулирует правило одной фразой: библиотека строит SQL через аргументные плейсхолдеры database/sql, которые автоматически экранируют значения — так SQL-инъекция отсекается на корню. Пользовательский ввод должен быть только аргументом: db.Where("name = ?", userInput) безопасен, даже если в userInput сидит «jinzhu;drop table users;» — драйвер отвезёт эту строку как значение, и база будет искать имя с точкой с запятой, а не исполнять её. Уязвимость появляется ровно в момент, когда ввод попадает в сам текст условия: db.Where(fmt.Sprintf("name = %v", userInput)) — вот это уже инъекция, потому что Sprintf вклеил строку в SQL до того, как её увидел драйвер. Кавычка внутри ввода закрыла строковый литерал — и хвост исполнился кодом. Плейсхолдеры — это не стиль и не вкусовщина, это граница между данными и кодом; всякий раз, когда соблазн склеить условие строкой «просто чтобы быстрее», вспоминайте этот абзац.

### Строковые условия и их SQL-след

```go
var todos []TodoItem

// Каноническая форма: условие с плейсхолдером и значение отдельным аргументом
db.Where("status = ?", "todo").Find(&todos)
// SELECT * FROM "todo_items" WHERE status = 'todo'

// Неравенство и сравнения - операторы обычного SQL
db.Where("priority <> ?", 0).Find(&todos)   // WHERE priority <> 0
db.Where("priority >= ?", 2).Find(&todos)   // WHERE priority >= 2

// Несколько условий в одной строке - через AND
db.Where("done = ? AND priority >= ?", false, 2).Find(&todos)
// WHERE done = false AND priority >= 2

// Короткая форма: колонка и значение двумя аргументами
db.Where("status", "doing").Find(&todos)
// WHERE status = 'doing'
```

След показывает главное: значение приехало в условие не склейкой, а подстановкой драйвера — в логе GORM печатает уже собранный SQL, но реально аргументы едут bind-переменными. Короткая форма удобна для мгновенных проверок, в рабочем коде чаще пишут с ? — единообразие помогает глазу.

### Что сделает такой запрос?

```go
// title - строка из формы пользователя:
// "хлеб'; DROP TABLE todo_items;--"
q := fmt.Sprintf("title LIKE '%%%s%%'", title)
db.Where(q).Find(&todos)
```

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

- ✅ SQL-инъекция: ввод вклеен в текст условия, кавычка сломала литерал, хвост уехал исполняться
- Ничего страшного: GORM сам экранирует строку условия перед выполнением
- Ошибка компиляции: Sprintf нельзя сочетать с Where
- Пустой результат: GORM отбросит подозрительное условие

Экранируются только аргументы плейсхолдеров; сам текст условия уезжает в SQL как есть. Sprintf вклеил пользовательский ввод прямо в литерал — классическая инъекция: кавычка из ввода закрыла строку, остальное стало командами. Безопасная форма — db.Where("title LIKE ?", "%"+title+"%"): шаблон склеивается в значении, а не в тексте условия. О том, чем это отличается и почему это безопасно, — в разделе про LIKE.

> **Пользовательский ввод — только в аргументы.** Правило из документации: не доверяйте присланному пользователем — используйте его только как аргумент, а для конечных множеств (например, колонок сортировки) делайте белые списки. При этом список методов, чьи строковые аргументы НЕ экранируются, длинный: Select, Distinct, Pluck, Group, Having, Raw, Exec, Order, Table, Delete, Joins — во всех них строка уезжает в SQL буквально. Условия Not и Or принимают те же формы, что и Where, и их строки — такой же сырой SQL: инъекция через Or или Not возможна точно так же, как через Where. Если колонка или направление сортировки приходит из запроса API — сверяйте его со списком разрешённых значений, а не вклеивайте в строку.

### Проверка: где место пользовательского ввода

**Вопрос:** Поисковая строка из запроса API попадает в переменную q. Какое обращение к базе безопасно?

- db.Where("title LIKE '%' || q || '%'") — условие целиком строкой
- db.Where(fmt.Sprintf("title LIKE '%%%s%%'", q)) — строка собрана до вызова
- ✅ db.Where("title LIKE ?", "%"+q+"%") — q едет значением плейсхолдера
- db.Where("title = " + q) — короткая запись без оператора

Только третий вариант: пользовательский текст сидит в аргументе, который драйвер экранирует, — даже если там кавычки и точка с запятой. В первом и четвёртом q вклеен в текст условия (конкатенацией), во втором — Sprintf-ом; во всех трёх это инъекция. Склейка шаблона LIKE над значением — легальна: конкатенация происходит в Go-значении, а не в SQL-тексте.

### Структурные условия: нулевые поля игнорируются

Вторая форма — структурная: db.Where(&TodoItem{Status: "todo"}).Find(&todos). BuildCondition разбирает структуру по схеме модели и на каждое ненулевое поле строит равенство: status = 'todo'. Удобство очевидно — колонки подставлены по схеме, опечатка в имени колонки ловится компилятором. Но у формы есть жёсткое правило, уже показанное в шестом уроке как ловушка: нулевые поля игнорируются. В Go «пустое» поле неотличимо от «не заполнял»: ноль, пустая строка, false — это zero values, и BuildCondition пропускает их через проверку isZero. Поэтому db.Where(&TodoItem{Priority: 0}) не даст WHERE priority = 0 — условие окажется пустым, и Find молча вернёт всю таблицу. Шестой урок обещал объяснить причину и дать второй фикс — вот они. Причина: структурные условия собираются только из ненулевых полей, ноль неотличим от «не заполнил». Первый фикс — карта: db.Where(map[string]interface{}{"priority": 0}) честно включает ноль, потому что карта не знает, что такое «пусто». Второй фикс — второй аргумент Where с перечнем полей, отдельный раздел ниже. Бонус формы: голый срез ключей как условие — db.Where([]uint{2, 4}) — разворачивается в WHERE id IN (2,4), это сахар над первичным ключом.

### Структура, карта и срез ключей

```go
// Структурное условие: каждое ненулевое поле - равенство
var active []TodoItem
db.Where(&TodoItem{Status: "todo", Priority: 3}).Find(&active)
// WHERE status = 'todo' AND priority = 3

// Ловушка из шестого урока: ноль - значит "не заполнял"
db.Where(&TodoItem{Priority: 0}).Find(&todos)
// WHERE нет вовсе: SELECT * FROM "todo_items" - вся таблица

// Карта включает нули; ключи сортируются по алфавиту
db.Where(map[string]interface{}{"priority": 0, "status": "todo"}).Find(&todos)
// WHERE priority = 0 AND status = 'todo'

// Срез ключей как условие - разворачивается в id IN
db.Where([]uint{2, 4}).Find(&todos)
// WHERE id IN (2,4)
```

Сравните второй и третий запросы: одна и та же логика «найти нулевой приоритет» — и два разных следа. Карта — честный способ искать по нулю. Деталь для отладки: ключи карты GORM сортирует, поэтому колонки в следе идут по алфавиту — след стабилен от запуска к запуску. Срез ключей — эквивалент Find(&todos, []uint{2,4}) из шестого урока.

### Проверка: что попадёт в WHERE?

**Вопрос:** Выполнили db.Where(&TodoItem{Done: false, Status: "todo"}).Find(&todos). Что окажется в WHERE?

- done = false AND status = 'todo': оба поля уехали в условие
- ✅ status = 'todo': false — zero value поля Done, в условие оно не едет
- done = false: условие по строке отброшено
- Пустое WHERE: вернутся все строки таблицы

False, 0 и пустая строка — zero values: структурное условие считает их «не заполнено». Status со значением "todo" ненулевой — он в WHERE. Хотите фильтр по done = false — берите карту map[string]interface{}{"done": false} или перечисляйте поля вторым аргументом. Это зеркальное отражение default-тегов пятого урока: там ноль подменялся дефолтом, здесь он выпадает из условия.

### Второй аргумент Where: Specify Struct search fields

Официальное имя приёма — Specify Struct search fields: передавая после структуры перечень полей, вы делаете сразу две вещи. Первая — белый список: в условие едут только перечисленные поля, остальные выпадают, даже если заполнены. Вторая — форс нулей: перечисленное поле едет в условие, даже если его значение нулевое. Документированный пример: db.Where(&User{Name: "jinzhu"}, "name", "Age") даст WHERE name = 'jinzhu' AND age = 0 — Age нулевой, но упомянут, поэтому включён. Имена в перечне можно смешивать: GORM сверяет каждый аргумент и с именем колонки (name), и с Go-именем поля (Age). Обратная сторона белого списка — рестрикт: всё, что не перечислили, игнорируется. Структура с пятью заполненными полями и перечнем из одного поля даст условие ровно по одному полю — забытая позиция в списке молча исчезает из WHERE. Приём стоит держать для точечных случаев «нужен ноль» и для явной фиксации состава условия; как замена обычной структурной форме он слишком коварен.

### Второй аргумент на практике

```go
// Белый список + форс нулей: оба поля едут, Priority - нулевой
db.Where(&TodoItem{Status: "todo", Priority: 0}, "Priority", "Status").Find(&todos)
// WHERE priority = 0 AND status = 'todo'

// Смешивать можно имя колонки и Go-имя поля
db.Where(&TodoItem{Title: "Купить хлеб", Priority: 2}, "Priority").Find(&todos)
// WHERE priority = 2 - Title заполнен, но в белый список не попал

// Без перечня та же структура дала бы оба условия:
// WHERE title = 'Купить хлеб' AND priority = 2
```

Первый запрос — лечение ловушки с нулём без карты: перечислили Priority — ноль уехал в WHERE. Второй показывает рестрикт: поле вне перечня выпадает, даже если заполнено. Проверяйте след каждого такого запроса: ошибка «забыл поле в списке» не ругается — просто тихо меняет выборку.

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

Третий кит — оператор IN: заменить лес OR-равенств одним условием со списком. Каноническая форма из документации — db.Where("status IN ?", []string{"todo", "doing"}): после IN идёт плейсхолдер, а значение — срез. При сборке Statement.AddVar разворачивает срез в скобки с bind-переменными — получается WHERE status IN ('todo','doing'), где каждый элемент списка по-прежнему едет экранированным аргументом. Круглые скобки в строке — форма "status IN (?)" — тоже допустимы: GORM поймёт, что скобки уже открыты, и не добавит вторые. Сращивать список в строку через strings.Join или Sprintf не нужно никогда: это и инъекция, если элементы от пользователя, и лишняя работа — срез GORM развернёт сам. Поведение на пустом срезе стоит знать наизусть: пустой срез печатается как IN (NULL), что ложно для любой строки — вернётся ноль записей и ни одной ошибки. Зеркальный оператор NOT IN пишется той же строкой ("status NOT IN ?") или через Not с картой, в которой лежит срез.

### IN и NOT IN

```go
// Каноническая форма: колонка IN ? и срез значений
var open []TodoItem
db.Where("status IN ?", []string{"todo", "doing"}).Find(&open)
// SELECT * FROM "todo_items" WHERE status IN ('todo','doing')

// Скобки в строке тоже допустимы:
db.Where("id IN (?)", []uint{1, 3, 5}).Find(&open)
// WHERE id IN (1,3,5)

// NOT IN - зеркальная форма той же строки
var notDone []TodoItem
db.Where("status NOT IN ?", []string{"done"}).Find(&notDone)
// WHERE status NOT IN ('done')

// Пустой срез - не ошибка, но и не совпадения:
var none []TodoItem
res := db.Where("id IN ?", []uint{}).Find(&none)
// WHERE id IN (NULL) - ложно для каждой строки
fmt.Println(len(none), res.RowsAffected, res.Error) // 0 0 <nil>
```

IN — самый частый оператор фильтров API: «показать задачи этих статусов». Обратите внимание на последний вызов: IN (NULL) не ошибка и не «вся таблица», а пустая выборка — NULL ни с чем не совпадает. Если пустой список — штатная ситуация, лучше не ходить в базу вовсе: ранний выход до запроса.

### Сколько строк вернёт запрос с пустым срезом?

```go
// Пользователь не выбрал ни одного статуса
statuses := []string{}
var picked []TodoItem
result := db.Where("status IN ?", statuses).Find(&picked)
fmt.Println(len(picked), result.RowsAffected, result.Error)
```

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

- ✅ 0 0 <nil>: уедет WHERE status IN (NULL), совпадений нет, ошибки нет
- 5 5 <nil>: пустой IN GORM отбросил, вернутся все строки
- 0 0 record not found: пустая выборка ошибка not found
- Паника: нельзя передавать пустой срез в условие

Пустой срез в строковой форме разворачивается в IN (NULL): сравнение с NULL ложно для каждой строки, итого ноль записей, Error — nil. ErrRecordNotFound здесь не при чём: Find, как объяснял шестой урок, пустую выборку ошибкой не считает. Нюанс на полях: голый срез-условие db.Where([]uint{}) ведёт себя иначе — условие исчезнет целиком и вернётся вся таблица; поэтому список проверяйте до запроса, а не надейтесь на SQL.

### LIKE и ILIKE: поиск по шаблону

Для поиска по подстроке SQL даёт LIKE с двумя спецсимволами: % — сколько угодно любых символов (в том числе ноль), _ — ровно один. Шаблон — это значение, а не часть условия: db.Where("title LIKE ?", "%хлеб%") — проценты сидят в аргументе, и их можно склеивать в Go: "%" + q + "%" безопасно, потому что склейка происходит над значением, которое драйвер потом экранирует (а вот % и _, пришедшие от пользователя, стоит вырезать или экранировать, если они не должны быть подстановками). С регистром всё зависит от диалекта, и здесь GORM не помощник — он передаёт текст условия базе как есть. PostgreSQL: LIKE регистрозависим, для поиска без учёта регистра у диалекта есть собственное расширение ILIKE — db.Where("title ILIKE ?", "%купить%") найдёт и «Купить хлеб». MySQL: при дефолтных коллациях *_ci LIKE регистронезависим сам по себе. SQLite: LIKE не различает регистр для ASCII-букв. Переносимый вариант руками — lower(title) LIKE lower(?), но функцию по колонке может лишить индекса — об этом разговор восемнадцатого урока.

### LIKE, ILIKE и регистр

```go
// Подстрока: % - сколько угодно символов
var bread []TodoItem
db.Where("title LIKE ?", "%хлеб%").Find(&bread)
// WHERE title LIKE '%хлеб%'

// _ - ровно один символ: пять знаков после дефиса
db.Where("slug LIKE ?", "buy-_____").Find(&bread)
// WHERE slug LIKE 'buy-_____' - подойдёт buy-bread

// ILIKE - расширение PostgreSQL: регистр не важен
db.Where("title ILIKE ?", "%купить%").Find(&bread)
// WHERE title ILIKE '%купить%' - найдёт и "Купить хлеб"
```

Первый запрос не найдёт «Купить хлеб» на PostgreSQL, если ищете «купить» в нижнем регистре: LIKE в этой базе чувствителен к регистру. ILIKE решает вопрос, но это ключевое слово PostgreSQL — на MySQL и SQLite его нет, там работают другие механизмы (коллации, upper/lower). Проверяйте диалект своей базы, а не надейтесь на универсальность.

### Проверка: регистр в поиске

**Вопрос:** PostgreSQL, в таблице есть задача «Купить хлеб». Какой вызов найдёт её по строке «купить» в нижнем регистре?

- db.Where("title LIKE ?", "%купить%") — LIKE в PostgreSQL не различает регистр
- ✅ db.Where("title ILIKE ?", "%купить%") — ILIKE, регистронезависимое расширение диалекта
- db.Where("title = ?", "купить") — равенство всегда без учёта регистра
- db.Where("title LIKE '%купить%'") — шаблон прямо в строке условия

LIKE в PostgreSQL регистрозависим: шаблон «%купить%» мимо «Купить хлеб». ILIKE — его регистронезависимый собрат, расширение именно этого диалекта. Равенство тем более точное. А четвёртый вариант даже с верным шаблоном — антипример: шаблон в тексте условия вместо аргумента, а если бы вместо «купить» стоял ввод пользователя — инъекция.

### Диапазоны и AND-склейка нескольких Where

Диапазонных операторов немного, и все — из SQL: >, >=, <, <= и BETWEEN ? AND ? с включительными границами. Время фильтруется теми же формами: db.Where("created_at > ?", weekAgo) — и плейсхолдер примет time.Time, драйвер сам переведёт его в таймстамп. Смысловая связка с chaining-ловушкой шестого урока: несколько Where в одной цепочке склеиваются AND — клозы сливаются в один WHERE по мере вызовов (MergeClause дописывает выражения в порядке вызова). Порядок вызовов отражается в SQL-следе, но на смысл чистых AND-условий не влияет — «A AND B» то же самое, что «B AND A»; влияет он только тогда, когда в цепочке появляется Or, о котором ниже. Напоминание одной фразой: инстанс после цепочки хранит уже добавленные условия — сохранить его в переменную можно только через Session(&gorm.Session{}), как показывал шестой урок. И ещё одна фраза на память: если в модели есть поле gorm.DeletedAt, GORM сам допишет к каждому WHERE условие deleted_at IS NULL — мягкое удаление молча сужает всякую выборку, детали девятого урока.

### Диапазоны и несколько Where

```go
// Диапазон числа: два клоза сравнения
var hot []TodoItem
db.Where("priority >= ?", 2).Where("priority <= ?", 3).Find(&hot)
// WHERE priority >= 2 AND priority <= 3

// BETWEEN - то же самое одной строкой, границы включительно
db.Where("priority BETWEEN ? AND ?", 2, 3).Find(&hot)
// WHERE priority BETWEEN 2 AND 3

// Дата-диапазон: задачи за последнюю неделю
weekAgo := time.Now().AddDate(0, 0, -7)
db.Where("created_at > ?", weekAgo).Find(&hot)

// Несколько Where склеиваются AND в порядке вызовов
db.Where("done = ?", false).Where("status IN ?", []string{"todo", "doing"}).Find(&hot)
// WHERE done = false AND status IN ('todo','doing')
```

BETWEEN — сахар над парой сравнений; выбирайте форму, которая читается лучше. Последний запрос — типовой каркас списка «открытые задачи в работе»: равенство плюс IN, склеенные двумя Where. Промежуточные вызовы без финишера ничего не отправляют базе — запрос уезжает только на Find.

### Шпаргалка: операторы условий

| Задача | Форма условия | SQL-след | Нюанс |
| --- | --- | --- | --- |
| Равенство | Where("status = ?", v) | status = 'todo' | ввод пользователя — только аргументом |
| Список значений | Where("status IN ?", срез) | status IN ('todo','doing') | пустой срез — IN (NULL): ноль строк |
| Исключить список | Where("status NOT IN ?", срез) | status NOT IN ('done') | то же даёт Not с картой и срезом |
| Подстрока | Where("title LIKE ?", "%х%") | title LIKE '%х%' | % и _ — спецсимволы, экранируйте из ввода |
| Поиск без регистра | Where("title ILIKE ?", ...) | title ILIKE '%...%' | расширение PostgreSQL, не для всех СУБД |
| Диапазон | Where("priority BETWEEN ? AND ?", 2, 3) | priority BETWEEN 2 AND 3 | границы включаются |
| Отрицание | Not("done = ?", true) | NOT done = true | по структуре даёт name <> value |

### Or и Not: где теряются скобки

Метод Or добавляет к WHERE OR-группу: db.Where("role = ?", "admin").Or("role = ?", "super_admin") даст WHERE role = 'admin' OR role = 'super_admin'. Опасность в том, что склейка выражений плоская: db.Where("done = ?", false).Where("priority >= ?", 2).Or("status = ?", "doing") соберёт строку done = false AND priority >= 2 OR status = 'doing' — а по приоритету операторов SQL это (done = false AND priority >= 2) OR status = 'doing'. Автор цепочки почти всегда хотел иначе: «открытые с высоким приоритетом, плюс всё, что в работе». Вывод: появился Or в цепочке — смотрите след и думайте скобками. Помощь есть: если условие с AND написано одной строкой, GORM сам обернёт его в скобки — db.Where("done = ? AND priority >= ?", false, 2).Or("status = ?", "doing") даст уже (done = false AND priority >= 2) OR status = 'doing'. Для сложных случаев документация предлагает Group Conditions — вложенные db.Where, которые строят настоящие скобки; это территория продвинутой части курса. Not работает зеркально Where: строка получит NOT перед условием, срез в карте превратится в NOT IN, а структурное условие превратит каждое равенство в <>. И да, их строки — такой же сырой SQL с теми же правилами инъекционной гигиены.

### Or, Not и их SQL-след

```go
// Плоский OR: Where + Where + Or
var flat []TodoItem
db.Where("done = ?", false).Where("priority >= ?", 2).Or("status = ?", "doing").Find(&flat)
// WHERE done = false AND priority >= 2 OR status = 'doing'
// по приоритету SQL это (done = false AND priority >= 2) OR status = 'doing'

// AND внутри одной строки - GORM сам поставит скобки
db.Where("done = ? AND priority >= ?", false, 2).Or("status = ?", "doing").Find(&flat)
// WHERE (done = false AND priority >= 2) OR status = 'doing'

// Not: отрицание строкового условия
var notDone []TodoItem
db.Not("status = ?", "done").Find(&notDone)
// WHERE NOT status = 'done'

// Not со срезом в карте - NOT IN
db.Not(map[string]interface{}{"status": []string{"done"}}).Find(&notDone)
// WHERE status NOT IN ('done')
```

Сравните первые два следа: условия те же, смысл разный — вся разница в скобках. Not в структурной форме даёт пары name <> value — это «все, кто не такой», а не «отрицание всего условия сразу»; для отрицания группы условий нужны скобки, то есть Group Conditions или ручная строка.

### Заполните пропуски: три формы условий

Восстановите Where с плейсхолдерами, карту с нулевым полем и регистронезависимый поиск.

```go
// Открытые задачи из списка статусов
var open []TodoItem
db.___("done = ?", ___).Where("status IN ___", []string{"todo", "doing"}).Find(&open)

// Поиск по нулевому приоритету: карта включает нули
db.Where(map[string]interface{}{"___": 0}).Find(&todos)

// Поиск без учёта регистра на PostgreSQL
db.Where("title ___ ?", "%купить%").Find(&todos)
```

Подсказка: Первый пропуск — сам метод цепочки; false — zero value, которое строковое условие честно включает; в строке условия за списком значений стоит ?; у карты ключ — имя колонки; ILIKE — расширение PostgreSQL.

Правильные ответы в пропусках: `Where`, `false`, `?`, `priority`, `ILIKE`

### Соберите путь WHERE по шагам

Расставьте шаги, которые проходят условия от вызова Where до выполнения запроса.

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

1. `Вызван db.Where("done = ?", false): строка с плейсхолдером и аргумент`
2. `BuildCondition распознал строку и собрал clause.Expr с переменными`
3. `Клоз WHERE добавлен в Statement методом AddClause`
4. `Второй Where слился с первым: MergeClause дописал выражение в клоз`
5. `Or добавил OR-группу в тот же клоз WHERE`
6. `При сборке SQL выражения склеены AND, OR-группа — через OR`
7. `Финишер Find собрал SELECT и выполнил запрос`

### Карточки: условия Where

- **Where("status = ?", v)** — Строковое условие: SQL-фрагмент плюс аргументы. Аргументы экранируются драйвером, сама строка — нет: инъекция возможна только при склейке. Несколько Where склеиваются AND.
- **Where(&TodoItem{...})** — Структурное условие: равенства по ненулевым полям. Zero values (0, "", false) игнорируются — Where(&TodoItem{Priority: 0}) даст пустой WHERE и всю таблицу.
- **Where(map[string]interface{}{...})** — Карта: колонки и значения, нули включаются честно. Ключи сортируются по алфавиту — SQL-след стабилен. Срез в карте разворачивается в IN.
- **Where(&TodoItem{...}, "Priority", "Status")** — Specify Struct search fields: белый список полей И форс нулей разом. Неперечисленные поля выпадают из условия, перечисленные едут даже нулевыми.
- **IN / NOT IN** — Where("status IN ?", срез): срез разворачивается в скобки с bind-переменными. Пустой срез — IN (NULL): ноль строк, ошибки нет. NOT IN — зеркально или через Not.
- **LIKE / ILIKE / Or / Not** — LIKE с % и _ — шаблон в аргументе; ILIKE — регистронезависимое расширение PostgreSQL. Or кладёт OR-группу (следите за скобками), Not отрицает: NOT cond, NOT IN, <>. 

### Что дальше по курсу

Условия gorm закрыты: три формы условия, плейсхолдеры и гигиена против SQL-инъекции, IN с его нулевым срезом, LIKE и ILIKE, диапазоны, Or и Not — это фундамент, на котором стоят все остальные выборки курса: и фильтры пагинации, и мягкое удаление, и связи. Восьмой урок применит те же условия к записи — обновления Update и Updates принимают WHERE ровно в тех формах, которые вы только что разобрали; текстом говорим заранее, ссылки не даём: урок ещё не написан. Если хочется освежить, откуда взялись пять задач и их SQL-следы, вернитесь к [пятому уроку про Create](/courses/gorm/lessons/05-create/); механику чтения и ловушку chaining — в [шестом уроке](/courses/gorm/lessons/06-read/); полный план из 25 уроков — на [странице курса](/courses/gorm/). Домашка ниже собирает весь арсенал в один скрипт — и одну намеренную инъекцию для наглядности.

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

- Строковое условие Where("col = ?", v) — SQL-фрагмент плюс аргументы: значения едут плейсхолдерами database/sql и экранируются; сама строка условия сырая, вклейте в неё ввод — получите SQL-инъекцию.
- Структурное условие собирается только из ненулевых полей: Where(&TodoItem{Priority: 0}) даст пустой WHERE и всю таблицу; нули включает карта или второй аргумент Where с перечнем полей (Specify Struct search fields).
- Второй аргумент Where работает в обе стороны: перечисленные поля едут в условие даже нулевыми, неперечисленные — выпадают.
- Where("status IN ?", срез) разворачивается в скобки с bind-переменными; пустой срез даёт IN (NULL) — ноль строк без ошибки; NOT IN — зеркальная форма или Not с картой.
- LIKE ищет по шаблону с % и _; ILIKE — регистронезависимое расширение PostgreSQL; поведение LIKE с регистром определяется диалектом, а не GORM.
- Диапазоны — >= <= и BETWEEN ? AND ? (границы включительно); несколько Where склеиваются AND в порядке вызовов; порядок не важен для смысла, пока в цепочке не появился Or.
- Or склеивает выражения плоско: A AND B OR C читается SQL-ом как (A AND B) OR C; условие с AND одной строкой GORM берёт в скобки сам; Not даёт NOT, NOT IN и <> по полям структуры.

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

Соберите локальный скрипт поверх таблицы из пятого урока и пройдите весь арсенал: три строковых условия с плейсхолдерами (=, <>, IN) и распечатайте их SQL-следы; структурное условие с нулевым Priority — убедитесь, что след пустой, затем повторите запрос картой и вторым аргументом Where; IN по списку статусов, затем тот же запрос с пустым срезом и проверка следа IN (NULL). Поищите задачу по title через LIKE с шаблоном в аргументе и через ILIKE — сравните регистр. Соберите диапазон priority BETWEEN 2 AND 3 и два Where через AND. Финал: соберите плоский Where+Where+Or, распечатайте result.Statement.SQL и объясните себе, почему выборка шире, чем казалась; почините строкой с AND внутри и сравните скобки. Отдельно сломайте намеренно: соберите условие fmt.Sprintf-ом с вводом «хлеб'; --» и опишите, что видите в следе.

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

### Можно ли подставить в Where имя колонки из пользовательского ввода?

Нет. Экранируются только значения плейсхолдеров; сама строка условия уезжает в SQL как есть, поэтому имя колонки от пользователя — это инъекция. Держите белый список допустимых имён в коде и выбирайте из него, а ввод используйте лишь как значение: Where("status = ?", userInput).

### Как искать по статусу, который может быть пустым списком?

Проверяйте список до запроса: если срез пуст, в базу можно не ходить — Where("status IN ?", срез) на пустом срезе даст WHERE status IN (NULL) и честный пустой результат без ошибки. Для голого среза-условия db.Where([]uint{}) условие исчезнет вовсе и вернётся вся таблица — вторая причина валидировать список заранее.

### Как объединить условия скобками, чтобы Or не сломал логику?

Для простой пары напишите AND-условие одной строкой: GORM сам возьмёт его в скобки перед OR-группой. Для произвольных вложений документация предлагает Group Conditions — вложенные вызовы db.Where, строящие настоящие скобки. И храните цепочки с условиями в переменной только через Session(&gorm.Session{}) — без неё условия прилипают к инстансу.
