Содержание урока · 15 разделов
- gorm scopes: контракт одной функции
- Механика: Statement.scopes и executeScopes
- Параметризованные скопы: замыкание
- Композитные условия: скопы как кирпичи
- Скопы-пагинация: связка с семнадцатым уроком
- ForList: именованный фильтр каталога
- Скопы для записи: раздел Updates
- Динамические таблицы: раздел Dynamically Table
- Встроенные скопы: что GORM даёт из коробки
- Четыре шаблона доки плюс ваша сборка
- Чек-лист урока
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
gorm scopes: контракт одной функции#
К восьмому хендлеру проекта возникает узнаваемая боль: каждый метод репозитория начинается с одного и того же — только невыполненные задачи, сортировка по времени, ограничение размера. Копировать эти три строки в десять мест значит гарантированно однажды поправить их в шести. GORM предлагает встроенный ответ на эту боль — скоупы. Страница доки scopes.html открывается определением: Scopes allow you to re-use commonly used logic, the shared logic needs to be defined as type func(gorm.DB) gorm.DB. Разберём фразу по частям. Во-первых, re-use commonly used logic — переиспользование повторяющейся логики, ровно та копипаста, от которой мы хотим уйти. Во-вторых, контракт: func(gorm.DB) gorm.DB — функция, принимающая цепочку и возвращающая цепочку. Это тот же самый тип, который имеют результаты методов Where, Order и Limit, поэтому скоп встаёт в любое место запроса и не отличим для базы от условий, написанных руками. В-третьих, никакого отдельного реестра, интерфейса или генерации кода: скоуп — это просто функция в вашем пакете, которую вы зовёте руками, передаёте аргументом и тестируете как любую другую.
Канон: StatusScope и второй кирпич
GO// Скоп - обычная функция с контрактом func(db *gorm.DB) *gorm.DB.
// StatusScope - базовый фильтр каталога: только невыполненные задачи.
func StatusScope(db *gorm.DB) *gorm.DB {
return db.Where("done = ?", false)
}
// Второй кирпич: фильтр приоритета. Один скоп - одно условие.
func PriorityScope(db *gorm.DB) *gorm.DB {
return db.Where("priority >= ?", 2)
}
// Применение: db.Scopes принимает любое число скопов через запятую.
var items []TodoItem
db.Scopes(StatusScope, PriorityScope).
Order("created_at DESC").Limit(20).Find(&items)
// SELECT * FROM todo_items
// WHERE done = false AND priority >= 2
// ORDER BY created_at DESC LIMIT 20Два скопа, два условия, одна выборка: db.Scopes перечисляет функции через запятую, база видит их как обычные WHERE-условия, склеенные через AND. Обратите внимание на плейсхолдеры — скоп обязан быть таким же безопасным, как ручной Where из седьмого урока, никакой конкатенации строк со значениями.
Механика: Statement.scopes и executeScopes#
Что физически делает db.Scopes? Смотрим в chainable_api.go, и ответ обескураживает своей краткостью: метод вызывает getInstance(), дописывает переданные функции в срез Statement.scopes и возвращает цепочку. Всё. Ни одна ваша функция в этот момент не вызывается — Scopes лишь копит их на будущее. Исполнение происходит позже, при финишере: в callbacks.go метод processor.Execute перед любой операцией крутит цикл for len(db.Statement.scopes) > 0 { db = db.executeScopes() }, а executeScopes очищает срез и по очереди прогоняет цепочку через каждую функцию. Из этой механики следуют три практических вывода. Первый: скопы исполняются при любом финишере — Find, First, Take, Count, Update, Delete, — потому что все они проходят через один и тот же processor.Execute. Второй: порядок гарантирован — функции применяются в том порядке, в каком перечислены в Scopes, и каждая работает с результатом предыдущей. Третий: условия скопов всегда оказываются в WHERE после условий, добавленных методами цепочки напрямую, — цепочку Where вы построили до финишера, а скопы выполняются уже внутри него. Срез Statement.scopes живёт внутри Statement, а не в конфиге, поэтому скопы — часть конкретной цепочки, а не глобального состояния.
Проверка: когда сработают скопы
Код вызвал db.Scopes(StatusScope, PriorityScope) и на этом остановился — Find не звали. Что уедет в базу?
Параметризованные скопы: замыкание#
Базовый скоп — функция без памяти: одно условие, одни и те же значения. Как только фильтр должен подстраиваться под ввод, включается вторая ступень — параметризованный скоп. Дока показывает его на заказах: func OrderStatus(status []string) func (db gorm.DB) gorm.DB возвращает func (db gorm.DB) gorm.DB, внутри которой return db.Scopes(AmountGreaterThan1000).Where("status IN (?)", status). Конструкция двухэтажная, и это самое важное: внешняя функция — фабрика, она принимает значение и возвращает скоп; значение фиксируется замыканием и живёт внутри внутренней функции. Вызов OrderStatus([]string{"paid", "shipped"}) — это уже готовый скоп, как StatusScope, только с зашитым внутрь списком статусов. Полный пример из доки собирает три кирпича: db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders) — заказы COD дороже тысячи, db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders) — оплаченные и отправленные дороже тысячи. Фабрики композируются свободно: один и тот же AmountGreaterThan1000 работает в обеих сборках. Замыкание здесь — не экзотика, а способ передать данные в фильтр, не протаскивая их через глобальные переменные и не ломая контракт func(gorm.DB) gorm.DB.
WithStatus: фабрика скопов на замыкании
GO// Модель заказов для примеров параметризованных скопов.
type Order struct {
ID uint
Amount int64
PayMode string
Status string
}
// Скоп из доки без параметров: сумма больше тысячи.
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
return db.Where("amount > ?", 1000)
}
// WithStatus - фабрика: принимает статус, возвращает скоп,
// в котором значение s зафиксировано замыканием.
func WithStatus(s string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", s)
}
}
// Композитный вызов: безымянный кирпич плюс фабрика.
var orders []Order
db.Scopes(AmountGreaterThan1000, WithStatus("paid")).Find(&orders)
// SELECT * FROM orders WHERE amount > 1000 AND status = 'paid'WithStatus — каноничная фабрика из задания курса: WithStatus(s string) func(db gorm.DB) gorm.DB. Внутри единственная роль внешней функции — запомнить s до вызова финишера. Дока решает ту же задачу фабрикой OrderStatus со срезом строк и условием status IN (?); принцип общий: параметры — аргументы фабрики, значения — в замыкании, SQL — с плейсхолдерами.
Что напечатает счётчик вызовов
Прочитайте код и выберите, что он выведет:
calls := 0
trace := func(db *gorm.DB) *gorm.DB {
calls++
return db.Where("done = ?", false)
}
tx := db.Scopes(trace)
fmt.Println(calls)
tx.Find(&items)
fmt.Println(calls)Композитные условия: скопы как кирпичи#
Настоящая сила подхода раскрывается на композитных условиях — сборках, где несколько фильтров работают одновременно. Правило сборки простое: Where-база и Scopes-фильтры не конкурируют, а складываются. База — это то, что вызывающий пишет руками: db.Where("priority >= ?", 2) как обязательный контекст конкретного хендлера. Фильтры — то, что вносится скопами: каждая функция дописывает свой клоз, и все они склеиваются через AND. Порядок выражений в WHERE предсказуем: сначала условия, добавленные методами цепочки, затем условия скопов — в том порядке, в каком скопы перечислены и в каком вызывались сами db.Scopes. Второе правило композиции: скоп может быть композитором, то есть звать Scopes сам. Так устроен OrderStatus из доки — его внутренняя функция начинает с db.Scopes(AmountGreaterThan1000), подтягивая чужой фильтр, и только потом добавляет своё условие по статусам. Так скоупы gorm образуют не плоский список, а дерево: мелкие кирпичи — одно условие, средние — комбинации, композитный скоп уровня ForList — готовая политика целой страницы. Логику ИЛИ не размазывайте по соседним скопам — каждый скоп склеивается с соседями через AND, поэтому ветвление прячьте внутрь одного фильтра группой условий, как показывал седьмой урок.
Проверка: два вызова Scopes
Что делает цепочка db.Scopes(StatusScope).Scopes(WithStatus("paid")) — два вызова подряд?
Скопы-пагинация: связка с семнадцатым уроком#
Семнадцатый урок уже показал скоп Paginate — и это не случайность, а эталон жанра: Paginate(r http.Request) func(db gorm.DB) gorm.DB читает page и page_size из параметров URL, защищает границы (page <= 0 превращается в единицу, pageSize <= 0 — в десятку, потолок — 100) и возвращает db.Offset(offset).Limit(pageSize) с формулой offset := (page - 1) pageSize. Здесь нам важен не пересчёт границ — он остался в семнадцатом уроке, — а сам факт: пагинация идеально ложится на формат скопа, потому что это универсальный, не зависящий от модели хвост запроса. Один и тот же Paginate обслуживает db.Scopes(Paginate(r)).Find(&users) и db.Scopes(Paginate(r)).Find(&articles), не зная ни про одну структуру. В связке с фильтрами получается полный конвейер страницы: db.Scopes(StatusScope, Paginate(r)).Find(&items) — отфильтруй, потом порежь на страницы. Для глубокой навигации по ленте offset из семнадцатого урока уступает курсору — keyset-условию WHERE id < ?, и это тоже собирается скопом: параметризованный фабричный фильтр с курсором плюс ограничитель размера. Метка «есть ещё» из семнадцатого урока — запрос pageSize+1 строк и срез хвоста — работает без изменений поверх любой такой сборки.
FeedScope: лента поверх индекса из восемнадцатого
GO// FeedScope - фабрика ленты: keyset-курсор поверх составного
// индекса idx_feed (done, created_at desc, id) из 18-го урока.
// lastTime - created_at последней строки предыдущей страницы.
func FeedScope(lastTime time.Time) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("done = ? AND created_at < ?", false, lastTime).
Order("created_at DESC").Order("id DESC")
}
}
// Ограничитель страницы: маленькая фабрика на один параметр.
func LimitScope(n int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB { return db.Limit(n) }
}
// Первая страница каталога: offset-пагинация из 17-го урока.
db.Scopes(StatusScope, Paginate(r)).Find(&top)
// SELECT * FROM todo_items
// WHERE done = false AND priority >= 2
// ORDER BY created_at DESC LIMIT 10 OFFSET 0
// Следующая страница ленты: курсор + размер, без OFFSET.
db.Scopes(FeedScope(lastTime), LimitScope(pageSize)).Find(&more)
// SELECT * FROM todo_items
// WHERE done = false AND created_at < ?
// ORDER BY created_at DESC, id DESC LIMIT 10Две стратегии из семнадцатого урока живут в мире скопов: offset-пагинация через Paginate(r) для первых страниц каталога и keyset-курсор через FeedScope(lastTime) для бесконечной ленты. FeedScope повторяет канонический запрос ленты из восемнадцатого урока — done = false, created_at < ?, сортировка created_at DESC, id DESC, — поэтому планировщик читает idx_feed, а не сканирует таблицу. LimitScope — фабрика на один параметр: обёртка над Limit, у которой тип совпадает с контрактом скопа.
ForList: именованный фильтр каталога#
Пора собрать всё в композитный скоп уровня политики — именованный фильтр целой страницы. В каталоге задач это ForList: только невыполненные, свежие вперёд, тай-брейк по ключу, фиксированный размер двадцать. Код — пять строк, но за ними стоит архитектурное решение: SQL-политика списка живёт в одном месте, и хендлеры перестают её дублировать. Композитный скоп устроен как OrderStatus из доки — внутри он зовёт Scopes: ForList подтягивает StatusScope как базу и добавляет сортировку с лимитом. Вызывающий код сокращается до db.Scopes(ForList).Find(&items), а точечные надстройки клеятся следом без редактирования ForList: db.Scopes(ForList, PriorityScope).Find(&items) — тот же список, но только с высоким приоритетом. Когда продакт просит показывать выполненные тоже, меняется одна строка в одном скопе — и все хендлеры курса обновляются. Это и есть переиспользование в строгом смысле: не «скопировать условие», а «сослаться на владельца условия». Отдельный бонус — тест: скоп чистая функция, ей можно подать DryRun-цепочку и сверить собранный SQL с эталоном, не трогая базу; DryRun из десятого урока здесь работает без изменений.
ForList: скоп, который собирает скопы
GO// ForList - композитный скоп каталога: одна точка правды
// для политики списка. Внутри сам зовёт Scopes, как OrderStatus
// из доки, и добавляет сортировку с тай-брейком по ключу.
func ForList(db *gorm.DB) *gorm.DB {
return db.Scopes(StatusScope).
Order("created_at DESC").Order("id DESC").
Limit(20)
}
// Три хендлера - одна политика:
var items []TodoItem
db.Scopes(ForList).Find(&items)
// точечная надстройка без правки ForList:
db.Scopes(ForList, PriorityScope).Find(&items)
// и даже курсорная лента поверх политики:
db.Scopes(FeedScope(lastTime)).Limit(pageSize).Find(&more)ForList — каноничный композитный фильтр из задания курса. Сортировка created_at DESC, id DESC и фильтр done = false попадают ровно под составной индекс ленты из восемнадцатого урока, поэтому политика не только переиспользуемая, но и быстрая: равенство вперёд, время по убыванию, ключ как тай-брейк.
Скопы для записи: раздел Updates#
Дока явно выделяет раздел Scope examples for updating/deleting — скопы работают не только на чтение. Канонический пример CurOrganization(r) решает типовую задачу API: разрезать таблицу по организации из параметров запроса. Внутри фабрики происходит важный трюк: org := r.Query("org"), затем db.Session(&Session{}).First(&organization, "name = ?", org) — внутренний запрос выполняется на свежем statement, который Session создаёт с нуля, поэтому поиск организации не подмешивает свои условия и скопы в ту цепочку, которую скопу передали. Если организация нашлась, фабрика возвращает db.Where("org_id = ?", organization.ID) — готовый фильтр по чужому ключу; если нет — db.AddError("invalid organization") и return db: это тот же механизм регистрации ошибки, что мы применяли в хуках шестнадцатого урока, цепочка доедет до финишера и вернёт ошибку наверх. Применение — оба финишера записи: db.Model(&article).Scopes(CurOrganization(r)).Update("Name", "name 1") превращается в UPDATE articles SET name = "name 1" WHERE org_id = 111, а db.Scopes(CurOrganization(r)).Delete(&Article{}) — в DELETE FROM articles WHERE org_id = 111. Скоп на записи — это страховка от самого дорогого бага ORM: массового апдейта или удаления, потерявшего WHERE.
Что за SQL у Delete со скопом
Прочитайте код и выберите, что он выведет:
// org найден: organization.ID = 111
// CurOrganization вернул db.Where("org_id = ?", 111)
db.Scopes(CurOrganization(r)).Delete(&Article{})Динамические таблицы: раздел Dynamically Table#
Четвёртый раздел доки — Use Scopes to dynamically set the query Table: скоп имеет право поменять не только WHERE, но и FROM. Фабрика TableOfYear(user User, year int) собирает имя таблицы конкатенацией user.TableName() + strconv.Itoa(year) и возвращает db.Table(tableName); вызовы DB.Scopes(TableOfYear(user, 2019)).Find(&users) и DB.Scopes(TableOfYear(user, 2020)).Find(&users) дают SELECT FROM users_2019 и SELECT FROM users_2020 — один и тот же код читает годовые секции таблицы. Соседняя фабрика TableOfOrg уводит таблицу в чужую базу: tableName := dbName + "." + user.TableName() — и DB.Scopes(TableOfOrg(user, "org1")).Find(&users) исполняет SELECT FROM org1.users. Приём стоит держать в загашнике для партиций по времени и мультитенантных схем, но с одной жёсткой оговоркой о безопасности: имя таблицы не параметризуется плейсхолдерами — Table вставляет его в SQL как есть, поэтому конкатенация годится только из доверенных источников, вроде внутренних справочников и конфигов. Значение, пришедшее из HTTP-параметра, обязано пройти белый список допустимых суффиксов, иначе скоп превратится в дыру: то же правило строк против белых списков, что и в седьмом уроке, только ставка выше — FROM инъецируется так же охотно, как WHERE.
Встроенные скопы: что GORM даёт из коробки#
Слово «встроенные» в заголовке требует честности: реестра именованных скопов, который можно было бы импортировать, в GORM нет — db.Scopes принимает исключительно функции с контрактом func(gorm.DB) gorm.DB, и строкой «paid» он воспользоваться не даст. Встроенное — это не список готовых фильтров, а три другие вещи. Первая — сама механика: срез Statement.scopes в цепочке, цикл executeScopes при каждом финишере и исполнение скопов даже внутри подзапросов — когда gorm.DB используется аргументом условия, statement.go явно вызывает v.executeScopes() на вложенной цепочке, так что фильтры сработают и там. Вторая — единая форма: любой метод-цепочка (Where, Order, Limit) возвращает gorm.DB, поэтому ваши скопы и встроенные методы взаимозаменяемы внутри одной сборки, а Session даёт свежий statement для внутренних запросов скопа, как в CurOrganization. Третья — четыре шаблона из доки, Query, Pagination, Dynamically Table и Updates, которые вместе покрывают практически весь спрос: фильтрация, страницы, таблицы, запись. Всё остальное — ваша территория, и в этом дизайн библиотеки: GORM не навязывает репозиторий, а даёт точку входа db.Scopes и контракт, который уживается с любой архитектурой — от простых хендлеров до слоёв репозиториев с собственным DSL фильтров, к чему мы и перейдём в следующем уроке.
Четыре шаблона доки плюс ваша сборка#
| Раздел доки | Фабрика или скоп | Что меняет в запросе | Пример SQL |
|---|---|---|---|
| Query | AmountGreaterThan1000, OrderStatus | WHERE-условия, склейка AND | SELECT * FROM orders WHERE amount > 1000 AND status IN ('paid','shipped') |
| Pagination | Paginate(r) | LIMIT и OFFSET из параметров URL | SELECT * FROM users LIMIT 10 OFFSET 10 |
| Dynamically Table | TableOfYear(user, 2019) | FROM — имя таблицы целиком | SELECT * FROM users_2019 |
| Updates | CurOrganization(r) | WHERE для UPDATE и DELETE | DELETE FROM articles WHERE org_id = 111 |
| Ваша сборка | ForList + WithStatus + Paginate | фильтр, сортировка, лимит одной строкой | SELECT * FROM todo_items WHERE done = false ORDER BY created_at DESC LIMIT 20 |
Проверка: а можно строкой
Коллега предлагает сократить запись: db.Scopes("paid") — передать статус строкой вместо функции. Что вы ответите?
Соберите путь скопа: от контракта до пакета
Расставьте шаги внедрения скопов в проект — от контракта до вынесения в пакет.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Заполните пропуски: фабрика и композитный скоп
Три пропуска: возвращаемый тип фабрики, метод добавления условия и метод сборки скопов.
Первая: скоп без финишера — мёртвый код. db.Scopes(...) лишь копит функции; пока не вызван Find, First, Count, Update или Delete, в базу не уедет ничего, и «почему не фильтрует» почти всегда значит «забыли финишер». Вторая: порядок в следе — условия методов цепочки стоят в WHERE раньше условий скопов, даже если db.Scopes вызван в цепочке первым: скопы исполняются в момент финишера, а не в момент написания. Третья: замыкание на переменной цикла — если собираете срез скопов в for и пишете func(db gorm.DB) gorm.DB { return db.Where("status = ?", s) } без фабрики, все замыкания делят одну переменную s; фабрика вида WithStatus(s) со параметром снимает вопрос сама. Четвёртая: скоп-кирпич со спрятанным Limit или чужим Order ломает композицию — Count поверх такого скопа посчитает строки уже обрезанной выборки, а HasNext из семнадцатого урока съедет; границы страницы держите отдельным скопом пагинации, как в примерах доки. Пятая: скоп обязан вернуть цепочку всегда — забытый return или nil в ветке обрушат executeScopes: следующая функция получит nil и запаникует; в спорных случаях действуйте как CurOrganization из доки — db.AddError(...) и return db, ошибка доедет до вызывающего штатно.
Чек-лист урока#
- контракт скопа — func(gorm.DB) gorm.DB; дока: Scopes allow you to re-use commonly used logic, the shared logic needs to be defined as type func(gorm.DB) gorm.DB
- db.Scopes(StatusScope, PriorityScope) перечисляет скопы через запятую; условия склеиваются через AND, плейсхолдеры обязательны
- механика: Scopes в chainable_api.go делает append в Statement.scopes, исполнение — executeScopes при финишере (цикл в processor.Execute, callbacks.go)
- скопы исполняются при любом финишере — Find, First, Take, Count, Update, Delete — и даже в подзапросах: statement.go вызывает v.executeScopes() на вложенной цепочке
- порядок WHERE: сначала условия методов цепочки, затем условия скопов в порядке передачи; два вызова Scopes подряд накапливают, а не затирают
- параметризованный скоп — фабрика, возвращающая замыкание: WithStatus(s string) func(db gorm.DB) gorm.DB; образец из доки — OrderStatus(status []string) со Scopes внутри
- композитный скоп — композитор: ForList зовёт Scopes(StatusScope) и добавляет сортировку с лимитом; SQL-политика списка живёт в одной точке
- четыре шаблона доки: Query — WHERE, Pagination — LIMIT/OFFSET, Dynamically Table — FROM (только доверенные имена!), Updates — WHERE для UPDATE и DELETE через AddError при неудаче
Что дальше по курсу#
Скоупы держатся за соседние темы курса всем составом. Условия, из которых сделан любой фильтр, — строковая и структурная формы, IN, LIKE, Or и Not — разобраны в седьмом уроке про Where. Скоп-пагинация Paginate с границами page и page_size, offset-формула и метка «есть ещё» без COUNT остались в семнадцатом уроке. Составной индекс ленты idx_feed, под который заточен FeedScope, и цена индексов при записи — в восемнадцатом уроке; карта всех тем курса — на странице курса. Следующий рубеж заберёт двадцатый урок — «Динамические запросы и builder-стиль»: когда фильтры приходят из API произвольным набором, заранее заготовленные скопы перестают угадывать форму запроса, и на сцену выходит ручная сборка клозов по условиям — безопасные фильтры API без копипасты и без инъекций. Замыкания и композитные кирпичи этого урока там не пропадут: builder тоже собирается из функций, просто ядро сборки переезжает с db.Scopes на явную работу с клозами.
Карточки: скоупы и переиспользование
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Скоп — функция с контрактом func(*gorm.DB) *gorm.DB; дока формулирует его как способ re-use commonly used logic, реестра именованных скопов в GORM нет.
- db.Scopes копит функции в Statement.scopes (chainable_api.go) и не вызывает их; исполнение — executeScopes при любом финишере: Find, First, Count, Update, Delete.
- Условия складываются через AND: база методов цепочки, затем скопы в порядке передачи; повторные вызовы Scopes накапливают; скоп может звать Scopes сам.
- Параметризованный скоп — фабрика с замыканием: WithStatus(s string) func(db *gorm.DB) *gorm.DB; образец доки — OrderStatus со status IN (?).
- Композитный ForList собирает политику страницы: StatusScope плюс сортировка created_at DESC, id DESC плюс лимит — хендлеры зовут один скоп.
- Лента FeedScope(lastTime) — keyset-курсор поверх idx_feed из восемнадцатого урока; рядом скопы Paginate(r) и LimitScope(n) из семнадцатого.
- Скопы работают на записи: CurOrganization режет UPDATE и DELETE по org_id, внутренний запрос — через Session(&Session{}), неудача — db.AddError и return db.
- Динамические таблицы (TableOfYear, TableOfOrg) меняют FROM; имя таблицы не параметризуется — только доверенные источники и белые списки.
Домашнее задание
Разверните канон локально и соберите пакет scops. Шаг первый: перенесите StatusScope, PriorityScope, WithStatus, FeedScope, LimitScope и ForList в отдельный файл, оставив хендлеры только с db.Scopes(...). Шаг второй: добавьте фабрику WithPayMode(mode string) по образцу доки и прогоните обе сборки — db.Scopes(AmountGreaterThan1000, WithPayMode("card")) и db.Scopes(AmountGreaterThan1000, WithPayMode("cod")), сверив SQL в следе логгера. Шаг третий: скопом CurOrganization по образцу доки разрежьте UPDATE и DELETE по организации и проверьте сценарий с несуществующим названием: ошибка invalid organization должна вернуться из финишера, а не из паники. Шаг четвёртый: DryRun-тестом из десятого урока сверьте, что ForList собирает WHERE done = false с ORDER BY created_at DESC, id DESC и LIMIT 20 в точности.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем скоп отличается от простого вызова Where в хендлере?
Формально — ничем для базы: скоп превращается в те же WHERE-условия. Практически — владением: условие в скопе имеет одну точку правды, покрыто тестом и меняется в одном месте, тогда как Where в каждом хендлере — десять копий, которые разъезжаются после первого же изменения требования.
Почему условия скопов появляются в WHERE после условий цепочки?
Потому что Scopes не исполняет функции немедленно — он дописывает их в Statement.scopes. executeScopes запускается внутри финишера, когда условия цепочки уже добавлены, поэтому след логгера всегда читается в порядке: Where-база, затем скопы по порядку передачи.
Можно ли передать в Scopes параметризованный скоп и как?
Нужно: Scopes принимает значения типа func(*gorm.DB) *gorm.DB, а фабрика WithStatus("paid") как раз такое значение и возвращает. Запись db.Scopes(WithStatus("paid")) вызывает фабрику прямо в месте применения; можно и заранее: paidScope := WithStatus("paid"), а потом db.Scopes(paidScope).
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
sqlc: типобезопасный код из SQL-запросов
Курс sqlc: настройка, запросы, параметры, NULL-типы, транзакции, джойны, пагинация, CI и сравнение с ORM. 25 уроков с практикой.
- Настраивать sqlc.yaml для Postgres/MySQL/SQLite
- Писать аннотированные запросы с типизированными параметрами
- Работать с NULL, массивами и JSON-колонками
- Организовывать транзакции через WithTx