Содержание урока · 13 разделов
- gorm first find: три финишера одной записи
- db.First по первичному ключу: inline-условия
- ErrRecordNotFound и идиома errors.Is
- First, Take, Last, Find: сравнение
- Take: сортировка ваша, а не базы
- Условия на минимум: Where и нулевые поля
- Сканирование: структуры, проекции select gorm, карты
- Pluck и Count: колонка и число
- Шпаргалка: каким финишером читать
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Пятый урок заполнил таблицу пятью задачами — теперь на них нужны запросы. Со стороны базы чтение — это SELECT, и по частоте он обгоняет всё остальное вместе взятое: на одну вставку в живом сервисе приходятся десятки выборок. В стандартной библиотеке каждый SELECT собирается руками: строка запроса, плейсхолдеры, rows.Next, ручное перекладывание колонок в поля структуры. GORM раздаёт эту работу финишерам: First, Take и Last достают одну запись — с LIMIT 1 и разными сортировками, Find — срез записей без ограничений, Pluck и Count — колонку и число. Одна честная оговорка о формате: СУБД в браузерной песочнице не живёт, поэтому runner-ов в курсе нет — вместо запуска вас проверяют quiz, predict, fill, order и карточки, а примеры рассчитаны на локальный Go с PostgreSQL или SQLite. Схема и данные — из четвёртого и пятого уроков; если делали домашку, таблица уже заполнена.
gorm first find: три финишера одной записи#
Начнём с внутренностей: в finisher_api.go библиотеки все три финишера одиночной записи устроены одинаково и отличаются буквально одной строкой. Каждый сначала ограничивает выборку — Limit(1), то есть LIMIT 1 в SQL: базе достаточно вернуть одну строку, лишнее сканирование отсечено. First и Last добавляют сортировку по первичному ключу — клоз ORDER BY с колонкой ключа: у First по возрастанию, у Last по убыванию (Desc: true в том же clause.OrderByColumn). Take сортировку не добавляет вообще — вернётся то, что база отдаст первой. Дальше все трое включают флаг RaiseErrorOnNotFound: если запрос не вернул ни строки, финишер вернёт ErrRecordNotFound, а не молчаливый ноль в RowsAffected. Find устроен скромнее: ни LIMIT, ни ORDER BY, ни флага ошибки — он выполняет условия и раскладывает результат в то, что вы передали. Отсюда правило выбора: одна запись с гарантированным порядком — First или Last, одна запись «как попало» или со своей сортировкой — Take, всё остальное — Find.
Первое чтение и SQL-след
GOvar first, taken, last TodoItem
result := db.First(&first) // ORDER BY "todo_items"."id" LIMIT 1
db.Take(&taken) // без ORDER BY: что база отдаст первой
db.Last(&last) // ORDER BY "todo_items"."id" DESC LIMIT 1
fmt.Println(first.Title, last.Title)
// Купить хлеб Обновить бюджет: ключи 1 и 5 из пятого урока
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
log.Println("таблица пуста")
}
// SQL-след (PostgreSQL):
// SELECT * FROM "todo_items" ORDER BY "todo_items"."id" LIMIT 1
// SELECT * FROM "todo_items" LIMIT 1
// SELECT * FROM "todo_items" ORDER BY "todo_items"."id" DESC LIMIT 1В таблице пять задач с ключами 1-5, поэтому First приносит «Купить хлеб», Last — «Обновить бюджет». Вызов Take без своего Order в комментариях мы даже не берёмся расшифровывать: недетерминированный он. Обратите внимание на приёмник: &first — указатель на нулевую структуру, GORM сам выделит и заполнит поля; проверка результата — через result.Error, как у Create.
db.First по первичному ключу: inline-условия#
Самый частый шаблон чтения — достать запись по ключу. Условие передаётся прямо в финишер, это inline-условия: db.First(&task, 3) построит WHERE id = 3 и добавит свой LIMIT 1 с сортировкой. Число можно отдать строкой — db.First(&task, "3") даст тот же запрос: аргумент приводится к ключу. Для строковых ключей вроде uuid форма другая — db.First(&task, "id = ?", "uuid-строка"): с плейсхолдером, как положено SQL. Срез ключей разворачивается в IN: db.Find(&todos, []int{1, 2, 3}) уедет с WHERE id IN (1,2,3). Есть и скрытая форма: если в переданной структуре ключ уже заполнен, он сам станет условием — var draft = TodoItem{ID: 10}; db.First(&draft) выполнит WHERE id = 10. Нюанс напоследок: сортировку по ключу First и Last знают, только когда им передали указатель на структуру или вызвали через db.Model(&TodoItem{}); на чужой приёмник вроде db.Table("todo_items").First(&map) связка не работает — документация прямо предупреждает, — а Take в той же ситуации работает, сортировать он и не собирается.
Что вернёт First на пустой таблице?
Прочитайте код и выберите, что он выведет:
var t TodoItem
err := db.First(&t).Error
fmt.Println(err, t.ID, t.Title)ErrRecordNotFound и идиома errors.Is#
ErrRecordNotFound — главная ошибка чтения, и проверять её принято канонически: errors.Is(result.Error, gorm.ErrRecordNotFound). Не через ==: это та же идиома работы с ошибками, что разобрана в курсе go-basics на уроке про несколько возвращаемых значений и ошибки — errors.Is разворачивает цепочку обёрток и находит ошибку, даже если между вами и оригиналом встал fmt.Errorf с %w или код диалекта. Прямое сравнение увидит только точный оригинал. Что делать дальше — два пути. Если «записи нет» — штатный сценарий (покажем пустой экран, отдадим 404, создадим черновик), ловите ошибку и ветвитесь. Если запись обязана существовать, а не-found — ЧП, берите альтернативу из документации: db.Limit(1).Find(&task) — Find флаг RaiseErrorOnNotFound не включает, ошибки not found не будет, проверяйте result.RowsAffected == 0. Только не пишите db.Find(&task) без Limit: такой запрос просканирует всю таблицу и вернёт первую попавшуюся строку — недетерминированно, без LIMIT 1, зато с лишней работой для базы.
Проверка: Take на пустой таблице
Таблица todo_items пуста. Выполнили var t TodoItem; err := db.Take(&t).Error. Чему равен err?
First, Take, Last, Find: сравнение#
| Финишер | ORDER BY | LIMIT | Пустой результат |
|---|---|---|---|
| db.First(&t) | первичный ключ по возрастанию | 1 | ErrRecordNotFound |
| db.Take(&t) | нет — как вернёт база | 1 | ErrRecordNotFound |
| db.Last(&t) | первичный ключ по убыванию | 1 | ErrRecordNotFound |
| db.Limit(1).Find(&t) | нет, если не задали | 1 | nil: RowsAffected 0 |
| db.Find(&t) в структуру | нет | нет | nil: первая попавшаяся строка |
| db.Find(&todos) в срез | нет | нет | nil: пустой срез, RowsAffected 0 |
Take: сортировка ваша, а не базы#
Зачем Take, если First предсказуемее? Затем, что сортировка по ключу — не единственный порядок в жизни. Когда порядок диктуете вы, внутренний ORDER BY только мешает: db.Order("priority DESC, created_at ASC").Take(&task) возьмёт самую приоритетную задачу, а не самую старую по id. Take же незаменим там, где модели нет: выборки из чужой таблицы через db.Table, приёмники-карты — GORM не знает схемы и не может угадать ключ, а Take и не пытается. Обратная сторона честности: без явного Order результат Take недетерминирован — «первая строка» зависит от плана запроса, физического порядка строк в таблице и версии оптимизатора. В тестах такое оставлять нельзя: либо Order, либо First. Практическое правило звучит так: Take — «мне нужна одна строка из этого набора; порядок описал я сам, или он не важен». Всё остальное — территория First и Last.
db.Find: срез задач
GOvar todos []TodoItem
result := db.Find(&todos) // SELECT * FROM "todo_items"
fmt.Println(len(todos), result.RowsAffected) // 5 5
fmt.Println(result.Error) // <nil>
// Условия можно передать прямо в Find - inline:
var open []TodoItem
db.Find(&open, "done = ?", false)
// SELECT * FROM "todo_items" WHERE done = false
// Срез ключей разворачивается в IN:
var picked []TodoItem
db.Find(&picked, []int{2, 4})
// SELECT * FROM "todo_items" WHERE id IN (2,4)
// Пустая выборка - не ошибка:
var nothing []TodoItem
res := db.Find(¬hing, "done = ?", true)
fmt.Println(len(nothing), res.RowsAffected, res.Error) // 0 0 <nil>Find — финишер наборов: срез не нужно инициализировать, GORM выделит память и допишет длину. RowsAffected совпадает с len среза — числом прочитанных строк. Главное отличие от одиночных финишеров: ErrRecordNotFound здесь не существует, пустая выборка — штатный nil-ответ с нулём в RowsAffected. Фильтровать можно и строкой-условием inline, и цепочкой Where — разница чисто стилистическая. Огромный набор, не влезающий в память целиком, читают родственником FindInBatches: порции по N записей с колбэком — вернёмся к нему, когда дойдём до больших выборок.
Сколько строк вернёт Find с условием?
Прочитайте код и выберите, что он выведет:
// В таблице 5 задач; две из них уже done = true
var closed []TodoItem
result := db.Find(&closed, "done = ?", true)
fmt.Println(len(closed), result.RowsAffected, result.Error)Условия на минимум: Where и нулевые поля#
db.Find(&open, "done = ?", false) — короткий путь; постоянная форма — цепочка Where: db.Where("done = ?", false).Find(&todos). В строковом условии плейсхолдер ? подставляется безопасно: параметры едут отдельно от текста запроса, тот же механизм, что и в database/sql. Вторая форма — структурная: db.Where(&TodoItem{Status: "todo"}).Find(&todos) соберёт WHERE status = 'todo' из непустых полей структуры. Здесь притаилась ловушка, зеркальная zero values из пятого урока: нулевые поля в структурном условии игнорируются — Where(&TodoItem{Priority: 0}) не даст WHERE priority = 0, условие окажется пустым, и Find молча вернёт всю таблицу. Карты вместо структур нули включают: db.Where(map[string]interface{}{"priority": 0}).Find(&todos) даст ровно priority = 0. Выборка вернула «слишком много»? Проверьте, не схлопнулись ли нули. Полный арсенал — IN, LIKE, диапазоны, Or, Not, группировки — ждёт в седьмом уроке про Where и операторы; здесь нужна была рамка для чтения, не больше.
Проверка: структурное условие с нулём
db.Where(&TodoItem{Priority: 0}).Find(&todos) — что попадёт в WHERE?
Сканирование: структуры, проекции select gorm, карты#
Как результат запроса попадает в ваши переменные? Сканирование идёт по конвенции из третьего урока: колонка snake_case соответствует полю CamelCase (title → Title), лишние колонки игнорируются, отсутствующие остаются нулями. Для полных моделей это db.First и db.Find из начала урока. Когда нужны только часть полей, работают проекции — те, кто ищет по запросу select gorm, имеют в виду ровно это: ограничить выборку колонками через Select и принять её лёгкой структурой-проекцией, приём Smart Select Fields из документации. Экономится трафик и память: тянуть Status, Priority и обе метки ради списка заголовков расточительно. Третий приёмник — карта: db.Model(&TodoItem{}).First(&row) с row map[string]interface{}{} разложит первую строку по именам колонок; для многих строк — срез карт. Model(&TodoItem{}) здесь обязателен: GORM должен знать, откуда читать таблицу. В карте лежат типы драйвера database/sql: числа int64, строки []byte, время time.Time — приводить придётся самим. Для динамики карта незаменима; в обычном коде я держусь структур — типы проверяет компилятор.
Проекция в структуру и в карту
GO// Проекция: только id и title, срез лёгких структур
type TodoCard struct {
ID uint
Title string
}
var cards []TodoCard
db.Model(&TodoItem{}).Select("id", "title").Scan(&cards)
// SELECT "id", "title" FROM "todo_items"
// Одна строка в карту: имена колонок становятся ключами
row := map[string]interface{}{}
db.Model(&TodoItem{}).First(&row)
// SELECT * FROM "todo_items" ORDER BY "todo_items"."id" LIMIT 1
fmt.Println(row["title"]) // Купить хлеб
// Много строк - срез карт
var rows []map[string]interface{}
db.Model(&TodoItem{}).Find(&rows)Scan — универсальный финишер сканирования: структура, срез структур, карта — приёмник может не совпадать с моделью, лишь бы имена полей совпали с колонками. Проекция TodoCard весит на порядки меньше полной записи — для списков и API-ответов это стандартный приём. С картой First работает только под Model: без модели GORM не знает таблицу — а вот сортировку по ключу при Model он добавит, как в SQL-следе.
Pluck и Count: колонка и число#
Два финишера для частичных результатов. Pluck вынимает одну колонку в срез: db.Model(&TodoItem{}).Pluck("id", &ids) положит в ids все ключи. Требование жёсткое: приёмник — срез, обычно примитивного типа; колонка — одна, не две. Нужны две колонки — это уже проекция: Select плюс Scan или Find, как в прошлом разделе. Имя GORM сначала ищет среди полей модели — Pluck("ID") поймёт, что речь о колонке id, — и Distinct с ним дружит: db.Model(&TodoItem{}).Distinct().Pluck("status", &statuses) даст SELECT DISTINCT "status", список уникальных статусов. Count считает строки: приёмник — указатель на int64, db.Model(&TodoItem{}).Count(&cnt) выполнит SELECT count(). Условия применяются как обычно: Where перед Count отфильтрует. Тонкость изнутри: count() ставится только если вы не задали свой Select, а после выполнения прежний клоз SELECT восстанавливается — цепочка продолжается без сюрпризов. И не считайте строки через len(среза): тянуть все записи ради числа — работа для базы, а не для вашего кода.
Pluck и Count на практике
GOvar ids []uint
db.Model(&TodoItem{}).Pluck("id", &ids) // [1 2 3 4 5]
var statuses []string
db.Model(&TodoItem{}).Distinct().Pluck("status", &statuses)
// SELECT DISTINCT "status" FROM "todo_items"
var cnt int64
db.Model(&TodoItem{}).Count(&cnt) // 5
db.Model(&TodoItem{}).Where("done = ?", false).Count(&cnt)
// SELECT count(*) FROM "todo_items" WHERE done = false
// Несколько колонок - уже не Pluck, а проекция:
var pairs []TodoCard
db.Model(&TodoItem{}).Select("id", "title").Scan(&pairs)Pluck и Count — самые дешёвые запросы в этом уроке: одна колонка или одно число вместо всей таблицы. Оба признают Distinct, оба уважают Where. Пара Pluck("id") с последующим Find по списку ключей — типовой каркас «сперва узнали, что читать»; его обобщение с пагинацией ждёт вас в уроках про выборки по страницам.
Шпаргалка: каким финишером читать#
- Запись по ключу или «самая старая/новая» — db.First / db.Last: LIMIT 1 и сортировка по первичному ключу.
- Одна строка со своей сортировкой или из чужой таблицы — db.Order(...).Take.
- Список записей, возможно пустой, — db.Find в срез: пустая выборка не ошибка.
- Одна строка без ошибки not found — db.Limit(1).Find в структуру и проверка RowsAffected.
- Одна колонка списком — Pluck в срез; количество строк — Count(&int64).
- Произвольный SELECT — Scan в структуру-проекцию или в map[string]interface{}.
Пятый урок обещал раскрыть её на чтении — вот документированный сценарий. queryDB := db.Where("done = ?", false) — и переменная поехала на два запроса. Первый, queryDB.Where("priority > ?", 2).Find(&a), отработает честно: WHERE done = false AND priority > 2. Второй, queryDB.Where("priority > ?", 5).Find(&b), тихо уедет с тремя условиями: WHERE done = false AND priority > 2 AND priority > 5 — старое условие осталось на инстансе и склеилось с новым. Если queryDB живёт в поле структуры или замыкании, весь сервис читает мусор, и никакая ошибка об этом не скажет. Лечение из документации: queryDB := db.Where("done = ?", false).Session(&gorm.Session{}) — сессия возвращает инстанс, безопасный для переиспользования: каждая новая цепочка стартует с чистым Statement. Либо дисциплина: цепочка начинается прямо с db и в переменные не сохраняется.
Проверка: сколько условий во втором запросе?
queryDB := db.Where("done = ?", false). Затем дважды: queryDB.Where("priority > ?", 2).Find(&a) и queryDB.Where("priority > ?", 5).Find(&b). Что окажется в WHERE второго запроса?
Заполните пропуски: чтение и подсчёт
Восстановите чтение по ключу с канонической проверкой ошибки и подсчёт строк.
Соберите путь запроса db.First
Расставьте шаги от вызова финишера до возврата результата — в том порядке, в котором их делает GORM.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: чтение записей
Что дальше по курсу#
Чтение закрыло вторую букву CRUD. Дальше — условия: седьмой урок посвящён Where и операторам (IN, LIKE, диапазоны, структурные условия и их подводные камни); сегодня вы видели их в минимальной дозе, там будет полный арсенал. Затем обновления, удаление и мягкое удаление с gorm.DeletedAt — которое, кстати, уже сейчас молча добавляет к каждому чтению фильтр по deleted_at, если поле есть в модели. Если хочется освежить, откуда взялись таблица, индексы и уникальный Slug, вернитесь к четвёртому уроку про миграции; вставки и их SQL-следы разобраны в пятом уроке; полный план из 25 уроков — на странице курса. Домашка ниже закрепляет чтение целиком — и одну осознанную поломку инстанса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- First, Take и Last достают одну запись: у всех троих LIMIT 1 и RaiseErrorOnNotFound; First сортирует по первичному ключу ASC, Last — DESC, Take не сортирует вовсе.
- db.First(&task, id) читает по ключу: inline-условия принимают число, строку и срез ключей (id IN); заполненный ключ в приёмнике сам становится условием.
- Пустой результат одиночного чтения — gorm.ErrRecordNotFound; каноническая проверка — errors.Is; обход без ошибки — db.Limit(1).Find, а не Find без Limit.
- db.Find(&todos) возвращает срез: пустая выборка — не ошибка, RowsAffected равен len; условия передаются inline или цепочкой Where.
- Структурные Where игнорируют нулевые поля: Where(&TodoItem{Priority: 0}) даст пустое условие и всю таблицу — нули включают картой или перечислением полей.
- Сканирование: колонка и поле связаны конвенцией; проекции — Select + Scan в лёгкую структуру; динамика — map[string]interface{}; Pluck — одна колонка в срез, Count — *int64.
- Ловушка chaining: инстанс после цепочки хранит условия, повторное использование склеивает их в запрос; лечится Session(&gorm.Session{}).
Домашнее задание
Соберите локальный пример поверх таблицы из пятого урока: прочитайте первую и последнюю задачу через First и Last, распечатайте SQL-следы и сравните сортировки; достаньте запись по существующему и по несуществующему ключу — второй случай проверьте errors.Is(err, gorm.ErrRecordNotFound). Соберите срез открытых задач через Find с условием и убедитесь, что пустой фильтр не возвращает ошибку. Сделайте проекцию id + title в лёгкую структуру и первую строку в map[string]interface{}. Посчитайте открытые задачи через Where и Count. Финал: воспроизведите chaining-ловушку — сохраните db.Where в переменную, сделайте два запроса с разными условиями и распечатайте result.Statement.SQL второго, затем почините через Session(&gorm.Session{}).
Личные заметкиПусто
Вопросы и ответы по уроку
Чем Take отличается от First, если ошибки у них одинаковые?
Только сортировкой: First добавляет ORDER BY по первичному ключу, Take не сортирует вообще. Когда порядок не важен или вы задаёте его собственным Order, Take честнее — не добавляет лишнего клоза. Флаг RaiseErrorOnNotFound и ErrRecordNotFound у обоих, как и у Last.
Почему db.Find не возвращает ErrRecordNotFound, когда записей нет?
Find — финишер для наборов: пустой результат — штатное состояние, а не ошибка. Флаг RaiseErrorOnNotFound у него не включён, поэтому Error остаётся nil, RowsAffected — 0. Ошибка not found зарезервирована за одиночными финишерами First, Take и Last.
Как безопасно переиспользовать запрос с условиями в разных хендлерах?
Сохраняйте сессию, а не цепочку: q := db.Where("done = ?", false).Session(&gorm.Session{}). Сессия возвращает инстанс, при использовании которого каждая новая цепочка получает копию Statement, а не дописывает условия в общий. Повторное использование инстанса без Session склеивает условия всех запросов — типовой источник «лишних» строк в выборках.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
sqlc: типобезопасный код из SQL-запросов
Курс sqlc: настройка, запросы, параметры, NULL-типы, транзакции, джойны, пагинация, CI и сравнение с ORM. 25 уроков с практикой.
- Настраивать sqlc.yaml для Postgres/MySQL/SQLite
- Писать аннотированные запросы с типизированными параметрами
- Работать с NULL, массивами и JSON-колонками
- Организовывать транзакции через WithTx