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

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

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

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

Чтение записей: First, Find — Take, Last, сканирование и ловушка chaining

Запрос gorm first find приводит сюда тех, кому нужно вытащить записи из базы: db.First по первичному ключу, db.Take без сортировки, db.Last в обратную сторону и срез через db.Find. Пятый урок положил пять задач в todo_items — теперь их надо прочитать. Разберём ErrRecordNotFound и идиому errors.Is, первые условия Where, сканирование в структуры и проекции select gorm, Pluck и Count — и ту самую ловушку chaining, о которой предупреждал пятый урок: на чтении она стреляет больнее всего.

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

Пятый урок заполнил таблицу пятью задачами — теперь на них нужны запросы. Со стороны базы чтение — это 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-след

GO
var 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 BYLIMITПустой результат
db.First(&t)первичный ключ по возрастанию1ErrRecordNotFound
db.Take(&t)нет — как вернёт база1ErrRecordNotFound
db.Last(&t)первичный ключ по убыванию1ErrRecordNotFound
db.Limit(1).Find(&t)нет, если не задали1nil: 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: срез задач

GO
var 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(&nothing, "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 на практике

GO
var 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) и queryDB.Where("priority > ?", 5).Find(&b). Что окажется в WHERE второго запроса?

Заполните пропуски: чтение и подсчёт

Восстановите чтение по ключу с канонической проверкой ошибки и подсчёт строк.

var task TodoItem err := db.(&task, 7).Error // чтение по первичному ключу if errors.(err, gorm.) { log.Println("задачи нет - покажем пустой экран") } var total db.Model(&TodoItem{}).(&total) // сколько записей в таблице

Соберите путь запроса db.First

Расставьте шаги от вызова финишера до возврата результата — в том порядке, в котором их делает GORM.

Строки

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

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

Карточки: чтение записей

Термин

db.First(&t, id)

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

Одна запись по первичному ключу: LIMIT 1 и ORDER BY id. Пустой результат — ErrRecordNotFound; проверять через errors.Is. Принимает ключ числом, строкой или условием.

1 / 6

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

Чтение закрыло вторую букву 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{}).

Личные заметкиПусто
Хранятся только в вашем браузере (GORM · Чтение записей: First, Find — Take, Last, сканирование и ловушка chaining)

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

Чем 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 склеивает условия всех запросов — типовой источник «лишних» строк в выборках.

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

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

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

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

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

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

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

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

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