---
title: "Скоупы и переиспользование запросов — func(db *gorm.DB) *gorm.DB, замыкания и композитные условия в GORM"
description: "gorm scopes: скопы как func(db *gorm.DB) *gorm.DB, базовые и параметризованные фильтры, замыкания, композитные условия и переиспользование запросов GORM."
course: "GORM: ORM-библиотека для Go"
url: "https://golangmaster.ru/courses/gorm/lessons/19-skoupy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Скоупы и переиспользование запросов — func(db *gorm.DB) *gorm.DB, замыкания и композитные условия в GORM

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

Восемнадцатый урок сделал ленту быстрой — пришла пора сделать её код удобным. gorm scopes — это встроенный механизм переиспользования запросной логики, и дока формулирует контракт одной фразой: Scopes allow you to re-use commonly used logic, the shared logic needs to be defined as type func(*gorm.DB) *gorm.DB. Любой фильтр — обычная функция, которая принимает *gorm.DB и возвращает *gorm.DB, а метод db.Scopes склеивает сколько угодно таких функций в один запрос. Скоупы gorm избавляют хендлеры от копипасты: базовый StatusScope, фабрика WithStatus с замыканием, композитный ForList, собирающий кирпичи в готовую политику списка, скопы-пагинация из семнадцатого урока и лента FeedScope поверх индекса из восемнадцатого — всё это читается как обычный Go и повторно используется без единой строчки SQL в вызывающем коде. В уроке — механика Statement.scopes и executeScopes из исходников, параметризованные фабрики, композитные условия, скопы для Update и Delete, динамические таблицы и честный разбор того, что в GORM «встроено», а что пишете вы. Одна оговорка о формате: GORM требует живую СУБД, поэтому runner-ов в уроке нет — код не исполнить в песочнице, вас проверяют quiz, predict, fill, order и карточки, а SQL-следы рассчитаны на локальный Go с PostgreSQL или SQLite.

### 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 не звали. Что уедет в базу?

- ✅ Ничего: Scopes только дописал функции в Statement.scopes, исполнит их executeScopes при вызове финишера
- SELECT выполнится сразу: Scopes прогоняет скопы в момент вызова
- Паника: Scopes без финишера — ошибка компиляции
- Запрос уедет при следующем AutoMigrate

Scopes в chainable_api.go делает ровно два действия: getInstance() и append в Statement.scopes. Исполнение отложено до processor.Execute в callbacks.go, который запускается финишерами — Find, First, Count, Update, Delete. Без финишера нет ни SQL, ни поездки в базу — та же ленивость, что и у любой цепочки GORM.

### Параметризованные скопы: замыкание

Базовый скоп — функция без памяти: одно условие, одни и те же значения. Как только фильтр должен подстраиваться под ввод, включается вторая ступень — параметризованный скоп. Дока показывает его на заказах: 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 — с плейсхолдерами.

### Что напечатает счётчик вызовов

```go
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)
```

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

- ✅ 0 и 1 — Scopes лишь копит функции, executeScopes запускает их один раз при финишере Find
- 1 и 1 — Scopes вызывает скоп немедленно, Find не пересчитывает
- 0 и 0 — Find исполняет условия напрямую, скопы остаются накопленными
- 1 и 2 — скоп вызывается и в Scopes, и в Find

В момент db.Scopes(trace) функция trace не вызывается — в Statement.scopes делается append, счётчик остаётся нулём. На Find включается processor.Execute, цикл вычищает срез и один раз прогоняет цепочку через trace — на экране ноль, потом единица. Это и есть отложенная механика скопов, наблюдаемая руками.

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

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

- ✅ Оба скопа применятся: каждый вызов дописывает функции в конец среза Statement.scopes
- Второй вызов затрёт первый: в Statement хранится ровно один скоп
- Компилятор не пропустит: Scopes нельзя вызывать дважды в одной цепочке
- Применится только первый: повторный вызов игнорируется

Statement.scopes — это срез []func(*DB) *DB, а Scopes делает append: tx.Statement.scopes = append(tx.Statement.scopes, funcs...). Два вызова подряд дают срез из двух функций, executeScopes применит их по порядку — сначала StatusScope, затем замыкание с 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 со скопом

```go
// org найден: organization.ID = 111
// CurOrganization вернул db.Where("org_id = ?", 111)

db.Scopes(CurOrganization(r)).Delete(&Article{})
```

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

- ✅ DELETE FROM articles WHERE org_id = 111 — скопы исполняются и в удаляющих финишерах
- DELETE FROM articles — условие скопа теряется: Delete игнорирует Scopes
- UPDATE articles SET deleted = true WHERE org_id = 111 — Delete со скопом мягко удаляет
- Запрос не выполнится: Scopes нельзя комбинировать с Delete

Это пример прямо из доки: db.Scopes(CurOrganization(r)).Delete(&Article{}) даёт DELETE FROM articles WHERE org_id = 111. Финишеры записи проходят через тот же processor.Execute, поэтому скопы применяются к UPDATE и DELETE так же, как к Find. Обратного API «скопы только для чтения» не существует — ограничивайте опасные скопы сами, на уровне ревью и прав доступа.

### Динамические таблицы: раздел 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") — передать статус строкой вместо функции. Что вы ответите?

- ✅ Так нельзя: Scopes принимает только значения типа func(*gorm.DB) *gorm.DB — обёртку-фабрику вроде WithStatus пишете сами
- Можно: Scopes сам построит условие по имени статуса
- Можно после import "gorm.io/scopes" — там реестр именованных скопов
- Нельзя в v1, но стало можно начиная с GORM v2

Сигнатура жёсткая: func (db *DB) Scopes(funcs ...func(*DB) *DB) (tx *DB) — тип функции-скопа и есть контракт из доки. Никакого реестра именованных скопов в GORM не существует: строка не скомпилируется, а удобный для вызывающих API строится фабриками — WithStatus("paid") возвращает нужный тип и остаётся типобезопасным.

### Соберите путь скопа: от контракта до пакета

Расставьте шаги внедрения скопов в проект — от контракта до вынесения в пакет.

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

1. `зафиксировать контракт: скоп — это функция func(db *gorm.DB) *gorm.DB, а не строка и не метод`
2. `нарезать базовые фильтры: StatusScope и PriorityScope, по одному условию на скоп`
3. `параметризовать повторяющиеся значения: фабрика WithStatus(s) возвращает замыкание`
4. `собрать композитный ForList: скоп, который сам зовёт Scopes и добавляет сортировку с лимитом`
5. `перевести хендлеры на db.Scopes(ForList, WithStatus("paid"), Paginate(r)).Find(&items)`
6. `сверить SQL в следе логгера: условия цепочки, затем скопы в порядке передачи`
7. `вынести скопы в отдельный пакет: SQL-политика живёт в одном месте, хендлеры её только зовут`

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

Три пропуска: возвращаемый тип фабрики, метод добавления условия и метод сборки скопов.

```go
// Фабрика: принимает статус, возвращает скоп-замыкание.
func WithStatus(s string) ___(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        return db.___("status = ?", s)
    }
}

// Композитный скоп каталога: база плюс сортировка с лимитом.
func ForList(db *gorm.DB) *gorm.DB {
    return db.___(StatusScope).Order("created_at DESC").Limit(20)
}
```

Подсказка: Фабрика возвращает func(db *gorm.DB) *gorm.DB; условие добавляет Where с плейсхолдером; чужой скоп подтягивается вызовом Scopes — так делает и OrderStatus из доки.

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

> **Пять ловушек скопов.** Первая: скоп без финишера — мёртвый код. 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](/courses/gorm/lessons/07-where/). Скоп-пагинация Paginate с границами page и page_size, offset-формула и метка «есть ещё» без COUNT остались в [семнадцатом уроке](/courses/gorm/lessons/17-paginaciya/). Составной индекс ленты idx_feed, под который заточен FeedScope, и цена индексов при записи — в [восемнадцатом уроке](/courses/gorm/lessons/18-indeksy/); карта всех тем курса — на [странице курса](/courses/gorm/). Следующий рубеж заберёт двадцатый урок — «Динамические запросы и builder-стиль»: когда фильтры приходят из API произвольным набором, заранее заготовленные скопы перестают угадывать форму запроса, и на сцену выходит ручная сборка клозов по условиям — безопасные фильтры API без копипасты и без инъекций. Замыкания и композитные кирпичи этого урока там не пропадут: builder тоже собирается из функций, просто ядро сборки переезжает с db.Scopes на явную работу с клозами.

### Карточки: скоупы и переиспользование

- **Контракт скопа** — Функция типа 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.Where, db.Order, db.Limit, db.Table; наружу — та же цепочка.
- **Механика Scopes и executeScopes** — chainable_api.go: Scopes делает tx = db.getInstance() и append(tx.Statement.scopes, funcs...) — функции не вызываются. callbacks.go, processor.Execute: for len(db.Statement.scopes) > 0 { db = db.executeScopes() } — исполнение при каждом финишере, по порядку передачи.
- **Параметризованный скоп** — Фабрика, возвращающая замыкание: func WithStatus(s string) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("status = ?", s) } }. Значение s фиксируется при создании; образец доки — OrderStatus(status []string) с db.Scopes(AmountGreaterThan1000).Where("status IN (?)", status).
- **Композитные условия и порядок** — Скопы складываются: база цепочки + фильтры скопов, склейка AND. Порядок WHERE: условия методов цепочки раньше условий скопов, скопы — в порядке передачи. Скоп может звать Scopes сам (композитор). Логику ИЛИ прячьте внутрь одного скопа группой условий.
- **Скопы на записи и ошибки** — Раздел Updates: db.Model(&article).Scopes(CurOrganization(r)).Update(...) и db.Scopes(CurOrganization(r)).Delete(&Article{}) — скопы работают на UPDATE/DELETE. Внутренний запрос — через db.Session(&Session{}); при неудаче db.AddError("invalid organization") и return db — ошибка доедет до финишера.
- **Динамические таблицы и встроенное** — TableOfYear(user, year): db.Table(user.TableName() + strconv.Itoa(year)) — SELECT * FROM users_2019; TableOfOrg — dbName + "." + таблица, SELECT * FROM org1.users. Имя таблицы не параметризуется — только белый список. Реестра именованных скопов нет: Scopes принимает только функции.

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

- Скоп — функция с контрактом 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).
