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

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

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

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

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

Восемнадцатый урок сделал ленту быстрой — пришла пора сделать её код удобным. 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.

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

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
QueryAmountGreaterThan1000, OrderStatusWHERE-условия, склейка ANDSELECT * FROM orders WHERE amount > 1000 AND status IN ('paid','shipped')
PaginationPaginate(r)LIMIT и OFFSET из параметров URLSELECT * FROM users LIMIT 10 OFFSET 10
Dynamically TableTableOfYear(user, 2019)FROM — имя таблицы целикомSELECT * FROM users_2019
UpdatesCurOrganization(r)WHERE для UPDATE и DELETEDELETE 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") — передать статус строкой вместо функции. Что вы ответите?

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

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

Строки

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

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

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

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

// Фабрика: принимает статус, возвращает скоп-замыкание. 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(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 на явную работу с клозами.

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

Термин

Контракт скопа

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

Функция типа 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; наружу — та же цепочка.

1 / 6

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

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 в точности.

Личные заметкиПусто
Хранятся только в вашем браузере (GORM · Скоупы и переиспользование запросов — func(db *gorm.DB) *gorm.DB, замыкания и композитные условия в GORM)

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

Чем скоп отличается от простого вызова 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).

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

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

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

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

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

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

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

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

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