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

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

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

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

Динамические запросы и builder-стиль — сборка WHERE по условиям и безопасные фильтры API в GORM

Девятнадцатый урок дал запросам память — скопы переиспользуют готовые фильтры, но предполагают, что форма запроса известна заранее. В реальном API всё наоборот: параметры каталога — статус, приоритет, выполненность, сортировка, страница — приходят произвольным набором, и заготовить скоп на каждую комбинацию невозможно. На сцену выходит gorm builder — динамические запросы в builder-стиле: цепочка собирается по условиям, каждый клоз добавляется отдельным if, значения едут плейсхолдерами. В уроке — сборка переменной и переприсваивание против chaining-ловушки из шестого урока, структурные условия с указателями и честная механика isZero из исходников, хелпер на []clause.Expression с групповыми условиями из доки, безопасные фильтры API с белым списком сортировки против инъекции в Order, пагинация и фильтры в одном конвейере, DryRun-тесты сборщика и честное сравнение скоупов с builder-стилем. Одна оговорка о формате: GORM требует живую СУБД, поэтому runner-ов в уроке нет — код не исполнить в песочнице, вас проверяют quiz, predict, fill, order и карточки, а SQL-следы рассчитаны на локальный Go с PostgreSQL или SQLite.

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

gorm builder: когда скопы перестают угадывать#

У каталога задач появился настоящий эндпоинт: /tasks?status=todo&priority=3&sort=created — и вот характер запроса перестал быть известным на этапе написания кода. Пять параметров дают десятки непустых комбинаций: со статусом и без, с приоритетом и без, с сортировкой и с дефолтом, плюс страница. Девятнадцатый урок отвечал на такую волатильность заготовками — кирпич-скоп на статус, фабрика на значение, композитный скоп на готовую политику списка, — и до поры до времени этого хватало. Но правило честное: как только комбинаций становится M на N, заготовленные скопы перестают угадывать форму запроса — надо было бы написать скоп на каждую перестановку, и реестр превратился бы в свалку. Ответ жанра — динамические запросы, то есть динамический WHERE: цепочка собирается по условиям прямо во время обработки запроса. Каждый клоз добавляет отдельный if: параметр пришёл — условие появилось, параметра нет — клоза нет. Это и называют builder-стилем: вы строите запрос по кирпичику из данных, а не выбираете из заранее собранных. Важная оговорка, чтобы не кидаться переписывать всё: builder не отменяет скопы из девятнадцатого урока — стабильные политики вроде «только невыполненные, свежие вперёд, двадцать штук» остаются именованными скопами; builder забирает себе волатильную часть — те клозы, что зависят от ввода пользователя. Седьмой урок дал строительный материал (три формы условия, плейсхолдеры, ИЛИ группой), шестой — предупреждение о хранении цепочки в переменной, семнадцатый — пагинацию; теперь всё это собирается в один конвейер безопасных фильтров API.

Канон: TodoFilter и сборка переменной

GO
// Фильтр каталога: нулевое значение поля означает "не задано".
type TodoFilter struct {
    Status   string // ""    - не фильтруем
    Priority int    // 0     - не фильтруем
    Done     bool   // false - не фильтруем
    Sort     string // имя сортировки из API
}

// buildQuery - сборщик динамического WHERE: база плюс клозы по условиям.
// Каждый сработавший if добавляет клоз, переменная переприсваивается.
func buildQuery(q *gorm.DB, f TodoFilter) *gorm.DB {
    q = q.Where("done = ?", false) // обязательная база каталога
    if f.Status != "" {
        q = q.Where("status = ?", f.Status)
    }
    if f.Priority != 0 {
        q = q.Where("priority >= ?", f.Priority)
    }
    return q
}

// Применение: сборка + сортировка с лимитом из семнадцатого урока.
var items []TodoItem
buildQuery(db, TodoFilter{Status: "todo", Priority: 2}).
    Order("created_at DESC").Limit(20).Find(&items)
// SELECT * FROM todo_items
// WHERE done = false AND status = 'todo' AND priority >= 2
// ORDER BY created_at DESC LIMIT 20

Три ключевые детали канона. Первая: фильтр — обычная структура, ноль поля означает «не задано», а не «фильтруй по нулю». Вторая: каждый if переприсваивает q — вы всегда работаете с последним инстансом цепочки. Третья: обязательная база done = false добавляется безусловно, поэтому даже при пустом фильтре WHERE не исчезнет — Find не вернёт всю таблицу целиком.

Сборка переменной: переприсваивание против ловушки#

Строка q = q.Where(...) выглядит как рискованное трюкачество — шестой урок приучил бояться сохранённых цепочек. Разберём, почему здесь дисциплина соблюдена. Механика из chainable_api.go: Where вызывает getInstance(), прогоняет условие через tx.Statement.BuildCondition(query, args...) и дописывает результат в Statement вызовом tx.Statement.AddClause(clause.Where{Exprs: conds}) — других действий нет. Переприсваивание q = q.Where(...) безопасно, потому что вы в каждой строке берёте свежий результат и старый инстанс тут же забываете: хранится ровно одна версия цепочки, и каждый клоз добавлен ровно один раз. Ловушка из шестого урока возвращается в другой раскладке: база собирается один раз в переменную, а финишируется из разных веток. Если из одного q сделать два Find — без переприсваивания, из двух мест кода, — второй запрос доедет с условиями первого: условия остались на инстансе и склеились. Builder-стиль закрывает проблему архитектурно: сборщик — функция, её зовут заново от db на каждый запрос, и каждая сборка стартует с чистого листа. Если же многоразовая база всё-таки нужна — храните инстанс после Session(&gorm.Session{}), как показывал шестой урок: каждая новая цепочка получит копию Statement, а не допишет условия в общий. Правило сборки переменной: один построенный запрос — один финишер; нужно два запроса — зови сборщик дважды или берись за Session.

Проверка: два финишера из одной базы

В хендлере сделали q := db.Where("done = ?", false), затем в одной ветке q.Where("priority >= ?", 3).Find(&a), в другой — q.Where("status = ?", "todo").Find(&b). Что вернёт второй запрос?

Указатели в фильтре: nil вместо флага#

Канонический TodoFilter скрывает дыру: bool не отличает «не задано» от «выполнено нет», а int — «не задано» от «приоритет ноль». База done = false спасает каталог, но представьте админский фильтр «показать только выполненные»: нужен предикат done = true при Done = true и отсутствие клоза при «не выбрано» — а у bool лишь два значения. Классическое решение Go — указатели: Status string, Priority int, Done bool, где nil означает «параметр не пришёл», а непустой указатель — «фильтруй по значению, даже если оно нулевое». Красота в том, что структурную форму Where можно оставить как есть — механика уже всё умеет. Седьмой урок показывал правило нулевых полей: BuildCondition собирает условие только по ненулевым полям, пропуская нули проверкой isZero. Заглянем глубже в исходники, чтобы понять, как указатели проходят эту проверку. В schema/field.go функция ValueOf возвращает значение поля и флаг: fieldValue.Interface(), fieldValue.IsZero(). Для указательного поля fieldValue — сам указатель, и IsZero() для него означает nil, а не разыменованный ноль! А BuildCondition в statement.go решает так: if v, isZero := field.ValueOf(stmt.Context, reflectValue); !isZero || selected — добавляет clause.Eq только для ненулевых полей. Итог: nil-указатель — ноль, клоза нет; bool, указывающий на false, — не ноль, условие done = false честно попадает в WHERE. Указатель = явное «поле выбрано», и false больше не теряется.

PtrFilter: nil пропускается, false доезжает

GO
// Указательный фильтр: nil - параметр не задан.
type PtrFilter struct {
    Status   *string
    Priority *int
    Done     *bool
}

done := false                      // из запроса: done=false
f := PtrFilter{Done: &done}        // Status и Priority - nil

var items []TodoItem
db.Where(&f).Order("created_at DESC").Find(&items)
// SELECT * FROM todo_items
// WHERE done = false ORDER BY created_at DESC

// Что произошло внутри statement.go:
// if v, isZero := field.ValueOf(...); !isZero || selected {
//     conds = append(conds, clause.Eq{Column: clause.Column{Table: curTable, Name: field.DBName}, Value: v})
// }
// ValueOf вернул fieldValue.IsZero(): для *bool это nil-проверка,
// &done не nil - флаг false - условие done = false в WHERE.

Один указатель на false дал полноценный клоз — структурная форма проигнорировала только nil-поля. В этом пункте часто путаются: нулевым для указательного поля считается nil, а не разыменованное значение. Хотите условие по отсутствующему параметру — не заполняйте указатель; хотите done = false — кладите адрес переменной.

Что уедет в базу

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

done := false
prio := 2
f := PtrFilter{Done: &done, Priority: &prio}

var items []TodoItem
db.Where(&f).Find(&items)

Builder-хелпер: []clause.Expression#

Строки с плейсхолдерами — не единственная валюта Where. В statement.go BuildCondition разбирает аргументы switch-ем, и среди веток — не только строки, карты и структуры: case clause.Expression: conds = append(conds, v) и case []clause.Expression: conds = append(conds, v...). Вторая ветка — готовый хук под builder-хелпер: соберите условия в срез выражений и отдайте весь срез одним вызовом db.Where(filterExprs(f)) — он развернётся в один WHERE-клоз с конъюнкцией через AND. Выражения типизированы: clause.Eq{Column: "status", Value: f.Status} — равенство, clause.Gte — «не меньше», clause.IN{Column: "status", Values: statuses} — перечисление, причём пустой срез значений даёт IN (NULL), как и строковая форма из седьмого урока. Для сложных предикатов есть групповые условия — отдельная фича доки: Group Conditions in GORM provide a more readable and maintainable way to write complex SQL queries involving multiple conditions. Пример из доки собирает вложенные цепочки: db.Where(db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")),).Or(db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),).Find(&Pizza{}) — и получает WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge"): вложенный db.Where выступает скобками. В пакете clause есть фабрики Or и And — clause.Or(expr1, expr2) складывает условия скобочной ИЛИ-группой прямо внутри одного клоза. Выражения композицибельны, как функции: хелпер возвращает []clause.Expression, его можно расширять, тестировать и переиспользовать независимо от цепочки.

filterExprs: сборка выражений и IN из параметра

GO
// Сборщик выражений: колонки - константы, значения - аргументы.
func filterExprs(f TodoFilter) []clause.Expression {
    var exprs []clause.Expression
    if f.Status != "" {
        exprs = append(exprs, clause.Eq{Column: "status", Value: f.Status})
    }
    if f.Priority != 0 {
        exprs = append(exprs, clause.Gte{Column: "priority", Value: f.Priority})
    }
    return exprs
}

// Срез выражений едит одним вызовом Where.
db.Where(filterExprs(f)).Where("done = ?", false).Find(&items)

// IN из списка статусов API: "status=todo,doing" -> срез строк.
statuses := strings.Split(f.StatusList, ",")
db.Where(clause.IN{Column: "status", Values: toAny(statuses)}).Find(&all)
// пустой список даст IN (NULL) - ничего не найдёт, как и в 07-м

// ИЛИ-группа внутри одного клоза: фабрика clause.Or.
q = q.Where(clause.Or(
    clause.Eq{Column: "status", Value: "todo"},
    clause.Gte{Column: "priority", Value: 3},
))
// WHERE (status = 'todo' OR priority >= 3)

Срез выражений — типизированная альтернатива строкам: BuildCondition разворачивает его в один клоз, склейка AND достаётся бесплатно. clause.IN повторяет поведение строковой формы на пустом срезе — IN (NULL). ИЛИ прячьте в один клоз фабрикой clause.Or — размазанный по цепочке Or теряет скобки, как показывал седьмой урок.

Проверка: почему срез выражений безопасен

В input юзера вписали jinzhu;drop table users; и передали в clause.Eq{Column: "status", Value: input}. Почему инъекции нет?

Безопасные фильтры API: белый список для сортировки#

Остался самый коварный параметр — Sort. У WHERE есть плейсхолдеры, у сортировки их нет: строка Order попадает в SQL как есть, и седьмой урок уже перечислял методы, чьи строковые аргументы не экранируются, а семнадцатый показывал подноготную — clause.Column{Name: v, Raw: true}. Страница Security ставит условие раньше всех примеров: User's input should be only used as an argument — ввод пользователя должен быть только аргументом запроса, а не частью строки SQL. Дока страхуется отдельным разделом: SQL injection Methods приводит db.Order("name; drop table users;").First(&user) в списке методов, куда ввод подставляется без экранирования, — сортировка соседствует с Select, Group, Table и Raw. Общее правило там сформулировано честно и широко: The general rule to avoid SQL injection is don't trust user-submitted data, you can perform whitelist validation to test user input against an existing set of known, approved, and defined input, and when using user's input, only use them as an argument. Белый список — это и есть безопасные фильтры API в чистом виде: пользователь выбирает не колонку, а имя из каталога, которое вы сами перевели в колонку. Реализация — карта имя-колонка: sortColumns := map[string]string{"created": "created_at", "priority": "priority", "title": "title"}. Параметр сначала ищется в карте; нашли — берём значение колонки, не нашли — это не ошибка БД, а понятный ответ клиенту: дефолтная сортировка или 400 с внятным сообщением. Направление сортируем отдельным белым списком из двух констант — asc и desc, а не «что пришло, то и в SQL». Пользовательский ввод в Order не должен появиться никогда — ни целиком, ни по частям.

sortColumns: имя API превращается в колонку

GO
// Белый список сортировки: имя из API -> колонка таблицы.
var sortColumns = map[string]string{
    "created":  "created_at",
    "priority": "priority",
    "title":    "title",
}

// Направление - тоже белый список из двух констант.
func sortClause(sort, dir string) (clause.OrderByColumn, bool) {
    col, ok := sortColumns[sort]
    if !ok {
        return clause.OrderByColumn{}, false // чужое имя - отказ
    }
    desc := dir == "desc" // всё, что не desc, - asc
    return clause.OrderByColumn{
        Column: clause.Column{Name: col}, Desc: desc,
    }, true
}

// Хендлер: Sort из API проходит карту, иначе - дефолт.
// Дефолт целиком: clause.OrderByColumn{Column: clause.Column{Name: "created_at"}, Desc: true}
order, ok := sortClause(f.Sort, "desc")
if !ok {
    order = clause.OrderByColumn{
        Column: clause.Column{Name: "created_at"}, Desc: true,
    }
}
buildQuery(db, f).Order(order).Limit(20).Find(&items)
// SELECT * FROM todo_items
// WHERE done = false AND status = 'todo'
// ORDER BY created_at DESC LIMIT 20

В SQL попадает только то, что лежит в карте, — строковая инъекция невозможна физически. clause.OrderByColumn — структурная форма сортировки из семнадцатого урока: она ставит кавычки вокруг имени за вас, а вы гарантируете, что имя существует в схеме. Тай-брейк по первичному ключу добавьте вторым Order — лента не будет терять строки на границах страниц.

Проверка: почему Order нельзя пускать сырым

Коллега предлагает упростить: buildQuery(db, f).Order(f.Sort).Find(&items) —.Sort ведь просто строка из запроса. Что не так?

Скопы против builder: честное сравнение#

Пришло время свести счета девятнадцатого урока с этим. Сначала неудобная правда: buildQuery(q gorm.DB) gorm.DB — функция с тем же контрактом, что у скопа; её можно передать в db.Scopes и она честно сработает. Разница не в механике, а в организации знания. Скоп — это именованная политика: фильтр живёт отдельно от вызывающего кода, тесты и правки в одной точке, вызов — одна строка. Builder — это конвейер: форма запроса вычисляется из данных на каждом вызове, if-ы видны прямо в сборщике. Выбирайте скоп, когда политика стабильна: «каталог — это невыполненные, свежие вперёд, двадцать штук» не зависит от ввода, и менять её на лету не нужно. Выбирайте builder, когда комбинаций много и все они равноправны: пять параметров API дают десятки сочетаний — заготовленный скоп на каждую перестановку будет свалкой, а builder описывает всё семейство одним алгоритмом. Честно и про цену: builder с десятью if-ами труднее читать, чем скоп в пять строк, — поэтому крупный builder разбивают на мелкие функции-условия, каждая из которых, кстати, снова скоп по форме. Гибрид работает лучше всего: волатильный динамический WHERE собирает buildQuery, а стабильные хвосты наклеиваются скопами — пагинация из семнадцатого урока ложится следом одной строкой, сортировка-белый список сидит в карте, обязательная база — в композитном скопе списка. Это не война парадигм, а деление труда: скопы для словаря политик, builder для произвольных фильтров API.

Скопы против builder: пять критериев#

КритерийСкопы (19-й)Builder-стиль (20-й)
Форма запросаизвестна заранее, стабильнавычисляется из ввода на каждом вызове
Комбинации фильтровнемного, именованные политикиM на N — один алгоритм вместо свалки скопов
Контрактfunc(db gorm.DB) gorm.DB, вызов через db.Scopesта же функция, но зовётся напрямую: buildQuery(db, f)
Условная логиказамыкание-фабрика на каждый параметрif-ы в одном сборщике, nil-указатели в структуре
Типовой гостькаталог с фиксированной политикой спискаэндпоинт /tasks?status=&priority=&sort=

Пагинация и фильтры вместе#

Настоящий список-эндпоинт отвечает двумя запросами: сколько всего и одна страница. Builder здесь раскрывается полностью: сборщик зову дважды, и оба раза от чистого db — ловушка сохранённой цепочки не при делах, потому что ни одна сборка не финишируется дважды. Первая сборка считает total: buildQuery(db.Model(&TodoItem{}), f).Count(&total) — те же фильтры, что и у страницы, поэтому число честное. Вторая собирает страницу: buildQuery(db, f).Order(order).Offset((page-1)size).Limit(size).Find(&items) — offset-формула и потолок размера страницы остались в семнадцатом уроке; туда же по ссылку, если нужно освежить границы и метку «есть ещё». Для глубокой ленты offset уступает курсору: добавьте в фильтр указатель-курсор и в buildQuery один if — if f.After != nil { q = q.Where("id < ?", f.After) } — и лента станет keyset-пагинацией, где сортировка обязана иметь тай-брейк по ключу. Обратите внимание, как органично курсор-указатель сочетается с темой nil-фильтров: первая страница приходит с nil-курсором и клоз не добавляется вовсе. Обе сборки делят один filterExprs и один sortColumns — фильтры и порядок вычисляются в одном месте, а Count и Find лишь выбирают, что с этим делать: посчитать или отрезать страницу.

listTasks: Count и страница из одного сборщика

GO
// listTasks - два запроса одного эндпоинта: total и страница.
func listTasks(db *gorm.DB, f TodoFilter, page, size int) (
    []TodoItem, int64,
) {
    var total int64
    // Сборка №1: те же фильтры, финишер Count.
    buildQuery(db.Model(&TodoItem{}), f).Count(&total)

    // Сборка №2: свежая цепочка от db, ловушки нет.
    order, ok := sortClause(f.Sort, "desc")
    if !ok {
        order = clause.OrderByColumn{
            Column: clause.Column{Name: "created_at"}, Desc: true,
        }
    }
    var items []TodoItem
    buildQuery(db, f).
        Order(order).
        Offset((page - 1) * size).Limit(size).
        Find(&items)
    return items, total
}
// SELECT count(*) FROM todo_items WHERE done = false AND status = 'todo'
// SELECT * FROM todo_items WHERE done = false AND status = 'todo'
//   ORDER BY created_at DESC LIMIT 20 OFFSET 20

Два вызова buildQuery — два независимых Statement: именно так безопасно строить парные запросы из одного фильтра. Count не видит Order и Limit — они на порядок не влияют; страница получает сортировку через белый список и окно из семнадцатого урока. Для бесконечной ленты замените Offset на курсорный if с указателем из предыдущего блока.

Соберите SQL глазами

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

f := TodoFilter{Status: "todo", Sort: "priority"}

var items []TodoItem
buildQuery(db, TodoFilter{}). // пустой фильтр
    Order("created_at DESC").
    Limit(20).
    Find(&items)

DryRun: тесты для сборщика#

Сборщик — чистая логика над клозами, и его хочется тестировать без базы. Это ровно режим DryRun из доки: DryRun Generates SQL without executing. It can be used to prepare or test generated SQL. Подаёте цепочке сессию db.Session(&gorm.Session{DryRun: true}) — финишер соберёт Statement и вернётся без поездки в СУБД, а след читается из stmt.SQL.String() и аргументы из stmt.Vars. Десятый урок уже показывал DryRun в деле; теперь у него системная работа — таблица тестов для сборщика: строка фильтров на входе, ожидаемый SQL и ожидаемые Vars на выходе. Собрали buildQuery — прогнали три случая: пустой фильтр с одной базой, полный набор клозов, чужое имя сортировки с дефолтом. Честности ради — две оговорки доки. Первая: разные диалекты генерируют разный SQL — PostgreSQL подставляет $1, MySQL знак вопроса, поэтому тест прикалывается к конкретному драйверу, как и SQL-следы этого курса. Вторая: итоговый SQL не всегда безопасно исполнять руками — GORM only uses it for logs, а из логгера строка выходит не полностью экранированной; тест сверяет плейсхолдеры и Vars, а не собранный со значениями текст. DryRun закрывает ещё один сценарий: логирование сборки в CI — падение теста покажет ровно тот клоз, который съел ваши фильтры.

Заполните пропуски: хелпер выражений

Три пропуска: тип среза выражений, поле равенства и метод-приёмник среза.

// Сборщик выражений: значение - аргумент плейсхолдера. func filterExprs(f TodoFilter) []clause. { var exprs []clause.Expression if f.Status != "" { exprs = append(exprs, clause.Eq{Column: "status", : f.Status}) } return exprs } // Срез выражений принимает один вызов. db.(filterExprs(f)).Find(&items)

Соберите путь запроса от HTTP до SQL

Расставьте шаги безопасного фильтра API — от разбора параметров до проверки следа.

Строки

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

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

Чек-лист урока#

  • gorm builder — сборка цепочки по условиям: каждый клоз добавляет отдельный if, нулевое поле фильтра означает «не задано»
  • механика Where из chainable_api.go: getInstance(), tx.Statement.BuildCondition(query, args...), tx.Statement.AddClause(clause.Where{Exprs: conds})
  • переприсваивание q = q.Where(...) безопасно; ловушка из шестого урока возвращается при двух финишерах из одной базы — лечение: сборщик зову заново или Session(&gorm.Session{})
  • указательные фильтры: nil — параметр не пришёл; fieldValue.IsZero() для указателя проверяет nil, поэтому *bool со значением false честно даёт done = false
  • Where принимает clause.Expression и []clause.Expression (ветки BuildCondition в statement.go); срез выражений — один клоз с AND
  • групповые условия доки: вложенные db.Where — скобки; clause.Or(expr1, expr2) — ИЛИ внутри одного клоза
  • сортировка из API — только через белый список: карта имя-колонка, направление из двух констант; Order в списке SQL injection Methods
  • правило доки: don't trust user-submitted data — whitelist validation и ввод только аргументом; DryRun-тесты сверяют stmt.SQL.String() и stmt.Vars без базы

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

Builder-стиль держится на соседних уроках всем составом. Материал клозов — строки с плейсхолдерами, структурная форма и её нулевые поля, карта, IN с пустым срезом, ИЛИ и скобки — это седьмой урок про Where. Окно страницы, offset-формула, курсор и тай-брейк сортировки остались в семнадцатом уроке — пагинация наклеивается на собранный builder-ом WHERE одной строкой. Кирпичи-политики, замыкания-фабрики и композитный скоп списка разобраны в девятнадцатом уроке — там же честный контракт func(db gorm.DB) gorm.DB, который builder унаследовал. Ловушка сохранённой цепочки и Session(&gorm.Session{}) — шестой урок, DryRun — десятый, индексы под сортировку и фильтры — восемнадцатый. Карта всех тем курса — на странице курса. Дальше строится мост к данным, которых в Go нет: двадцать первый урок — «Работа с NULL и указателями» — заберёт указательные фильтры этого урока в модель и покажет sql.Null-типы, nullable-теги и то, как NULL из базы превращается в ноль или указатель в коде. Динамические запросы собраны — теперь научимся честно хранить отсутствующие значения.

Карточки: builder-стиль и фильтры API

Термин

Сборка переменной (gorm builder)

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

q := db.Where("done = ?", false); if f.Status != "" { q = q.Where("status = ?", f.Status) } — каждый if переприсваивает q. Один построенный запрос — один финишер. Два финишера из одной базы = chaining-ловушка шестого урока: лечение Session(&gorm.Session{}) или двойной вызов сборщика buildQuery(db, f).

1 / 6

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

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

Вывод

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

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

  • gorm builder — динамические запросы: цепочка собирается по условиям, каждый клоз добавляет отдельный if, обязательная база добавляется безусловно и не даёт WHERE опустеть.
  • Механика Where: getInstance(), BuildCondition(query, args...) и AddClause(clause.Where{Exprs: conds}) — Where лишь копит клозы, финишер собирает SQL.
  • Переприсваивание q = q.Where(...) безопасно; два финишера из одной базы — chaining-ловушка шестого урока: сборщик зову заново от db или база через Session(&gorm.Session{}).
  • Указательные фильтры: nil пропускается, адрес на false даёт done = false — fieldValue.IsZero() для указателя проверяет nil, а не разыменованное значение.
  • Where принимает []clause.Expression одной веткой BuildCondition: clause.Eq, clause.Gte, clause.IN, ИЛИ — фабрикой clause.Or внутри одного клоза.
  • Сортировка из API — только белый список: карта имя-колонка плюс направление из двух констант; Order в списке SQL injection Methods, его строки не экранируются.
  • Пагинация и фильтры вместе: Count и страница — два вызова сборщика от чистого db; для ленты — курсорный if с указателем и тай-брейк по ключу.
  • Скопы и builder делят труд: стабильные политики — именованные скопы девятнадцатого урока, произвольные комбинации из API — сборка по if-ам; тесты — DryRun со сверкой SQL и Vars.

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

Соберите пакет buildfilter поверх локального канона. Шаг первый: перенесите TodoFilter, buildQuery и sortColumns, добавьте PtrFilter с указательными полями и проверьте на локальной базе, что db.Where(&PtrFilter{Done: &done}) при done = false даёт WHERE done = false, а nil-поля не дают ничего. Шаг второй: добавьте filterExprs с clause.Eq и clause.Gte и переключите buildQuery на срез выражений, сверив след с прежним через DryRun: db.Session(&gorm.Session{DryRun: true}) и stmt.SQL.String(). Шаг третий: заведите эндпоинт /tasks?status=&priority=&sort=&dir= и проверьте белый список: sort=created даёт ORDER BY created_at DESC, sort= hacked; -- даёт дефолт и не ломает SQL. Шаг четвёртый: напишите listTasks с Count и страницей двумя сборками и таблицу тестов DryRun: пустой фильтр, полный, чужая сортировка — SQL и Vars против эталона. Финал: добавьте курсорный if с указателем и превратите ленту в keyset-пагинацию из семнадцатого урока.

Личные заметкиПусто
Хранятся только в вашем браузере (GORM · Динамические запросы и builder-стиль — сборка WHERE по условиям и безопасные фильтры API в GORM)

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

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

Механика одна — та же функция с контрактом func(db *gorm.DB) *gorm.DB. Разница в организации: скоп — именованная стабильная политика, вызываемая через db.Scopes; builder — алгоритм, вычисляющий клозы из ввода на каждом вызове. Скопы выигрывают на фиксированных политиках, builder — когда комбинаций фильтров M на N и форма запроса приходит из API.

Почему Where с указателями пропускает false, а без указателей — нет?

Проверка нулевых полей идёт через fieldValue.IsZero() в schema/field.go. Для обычного bool значение false нулевое — поле выпадает из условия. Для *bool поле хранит указатель, и IsZero() проверяет nil: непустой указатель на false не нулевой, поэтому clause.Eq с done = false попадает в WHERE. Nil-указатель по-прежнему пропускается.

Можно ли доверить Order строку из запроса, если она выглядит как имя колонки?

Нельзя: строковый аргумент Order не экранируется — в chainable_api.go он оборачивается в clause.Column{Name: v, Raw: true} и вписывается в SQL как есть; дока приводит db.Order("name; drop table users;") в списке SQL injection Methods. Единственный безопасный путь — белый список: карта имя-колонка и отдельный список направлений asc и desc.

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

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

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

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

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

sqlc25 уроков~12 чпрактика25 уроков уже доступны

sqlc: типобезопасный код из SQL-запросов

Курс sqlc: настройка, запросы, параметры, NULL-типы, транзакции, джойны, пагинация, CI и сравнение с ORM. 25 уроков с практикой.

  • Настраивать sqlc.yaml для Postgres/MySQL/SQLite
  • Писать аннотированные запросы с типизированными параметрами
  • Работать с NULL, массивами и JSON-колонками
  • Организовывать транзакции через WithTx