PostgreSQL CookbookПодключение и ориентацияТипизированные запросы через sqlc
0 / 63 (0%)

Типизированные запросы через sqlc

В 00-04 мы прочитали меню Brew из Go руками: pool.Query, цикл rows.Next, rows.Scan в поля структуры по порядку. Код рабочий, но хрупкий. Поменялся порядок колонок в SELECT — и Scan молча положит name в поле category: компилятор не возразит, потому что обе колонки text. Забыл rows.Err() — пропустишь ошибку чтения. Это не гипотетика, а класс багов, которые всплывают в проде, а не на ревью.

Цель юнита — закрыть этот класс целиком: пишем SQL в query.sql, запускаем sqlc generate, и получаем типизированный Go-код, в котором порядок и типы колонок сверены со схемой на этапе сборки. 00-02 уже показал этот конвейер в общих чертах; здесь мы разбираем его как рабочий инструмент — с параметром $1, тремя формами результата и тем самым boilerplate'ом из 00-04, который теперь генерируется.

Что такое sqlc (и чем он не является)

sqlc — это кодогенератор, а не ORM и не драйвер. Он берёт два входа: схему (DDL твоих таблиц) и query.sql (запросы, которые ты написал руками). Парсит и то и другое, понимает, какие колонки и каких типов вернёт каждый запрос, и генерирует Go-функции, которые этот запрос выполняют и раскладывают результат в типизированные структуры.

Принципиально: SQL остаётся твоим. sqlc не прячет запросы за «магическим» API вроде .Where("category", cat).First() — ты по-прежнему пишешь SELECT ... WHERE category = $1, и именно этот SQL уезжает на сервер. sqlc убирает только механический разбор строк — тот самый цикл Scan, который мы писали в 00-04.

Это не ORM: sqlc не управляет связями, не делает lazy loading, не строит запросы динамически и не накатывает миграции (схемой и миграциями займётся модуль 02). Он делает ровно одно — превращает написанный руками SQL в типобезопасный Go. Поэтому SQL и остаётся в центре курса.

query.sql: аннотации и формы результата

Запрос для sqlc — это обычный SQL плюс строка-аннотация над ним:

sql
-- name: ListDrinksByCategory :many
SELECT id, sku, name, category, base_price
FROM drinks
WHERE category = $1
ORDER BY id;

-- name: ListDrinksByCategory задаёт имя метода. Суффикс — форму результата:

СуффиксЧто возвращает методКогда брать
:many[]XxxRow — срез строк (пустой, если совпадений нет)SELECT, отдающий 0..N строк
:oneXxxRow — одна строка (или pgx.ErrNoRows, если её нет)SELECT ровно одной строки
:one (скаляр)сам тип колонки: count(*)int64, без обёртки-структурыодна колонка в одной строке
:execтолько errorINSERT/UPDATE/DELETE без RETURNING

А $1 — параметр из 00-04, но теперь о нём заботится sqlc. Глядя на схему, он видит, что drinks.category — это text, и генерирует метод с аргументом category string. Имя параметра он берёт из колонки в условии — поэтому метод читается как ListDrinksByCategory(ctx, category string), а не (ctx, arg1 string).

Что показывает наш код

Из трёх запросов в query.sql sqlc сгенерировал три метода. Сигнатуры (из internal/db/querier.go) говорят сами за себя:

go
ListDrinksByCategory(ctx, category string) ([]ListDrinksByCategoryRow, error)  // :many
GetDrinkBySKU(ctx, sku string)             (GetDrinkBySKURow, error)            // :one
CountDrinksByCategory(ctx, category string) (int64, error)                      // :one (скаляр)

Параметры типизированы (category string, sku string), результат — тоже: :many отдаёт срез структур, :one — одну структуру, а :one со скаляром (count(*)) — сразу int64, без лишней обёртки. Внутри сгенерированного ListDrinksByCategory лежит ровно тот цикл Query → rows.Next → Scan → rows.Err из 00-04 — только написал его не ты, а генератор, и ошибиться в порядке Scan он не может.

main.go после этого — тонкий, как в 00-02:

go
queries := db.New(pool)
coffee, err := queries.ListDrinksByCategory(ctx, "coffee")   // :many
cold, err := queries.GetDrinkBySKU(ctx, "CLD-01")            // :one
teaCount, err := queries.CountDrinksByCategory(ctx, "tea")   // :one, скаляр

Никакого rows.Scan, никаких SQL-строк в Go-коде. Если завтра ты добавишь колонку в SELECT и забудешь обновить разбор — после make gen тип просто изменится, и компилятор покажет все места, где сигнатура разъехалась. В этом вся разница с 00-04: ошибка ловится сборкой, а не пользователем.

Запуск

Подними песочницу (из корня репозитория) и накати схему Brew:

sh
docker compose up -d
make lecture L=00-getting-connected/00-05-typed-queries-with-sqlc T=db-reset

Перегенерируй код из query.sql (по желанию — он уже закоммичен) и запусти демо:

sh
make lecture L=00-getting-connected/00-05-typed-queries-with-sqlc T=gen
make lecture L=00-getting-connected/00-05-typed-queries-with-sqlc

(T=run — значение по умолчанию. Изнутри каталога юнита это make gen, make db-reset, make run.)

Вывод:

plaintext
1) ListDrinksByCategory("coffee") — :many, $1 типизирован как string:
ID  SKU     НАЗВАНИЕ  КАТЕГОРИЯ  ЦЕНА
1   ESP-01  Эспрессо  coffee     3.00
2   CAP-01  Капучино  coffee     4.50
3   LAT-01  Латте     coffee     4.80
 
2) GetDrinkBySKU("CLD-01") — :one, одна строка:
   #4  CLD-01  Колд брю  (cold)  5.20
 
3) CountDrinksByCategory("tea") — :one, скаляр int64:  1

:many вернул три напитка категории coffee, :one — одну строку по SKU, :one-скаляр — число 1 (зелёный чай в меню один). Те же данные, что в 00-04, но из кода исчез весь ручной разбор.

Заборчик

  • sqlc типобезопасен ровно настолько, насколько правдива его схема. Он сверяет запросы с тем DDL, что перечислен в sqlc.yaml (../../../schema/brew.sql + schema.sql), — если реальная база разошлась со схемой в файлах, sqlc об этом не узнает (он не подключается к БД при генерации). Поэтому источник правды о структуре — миграции (модуль 02), а файлы схемы в sqlc.yaml должны идти с ними в ногу.
  • sqlc — дефолт курса, но не догма. Когда запросу нужны системные колонки (xmin/ctid), динамический SQL, EXPLAIN или интерактивные сессии — sqlc не подходит, и урок переключается на escape-hatch (raw pgx или psql-скрипты, как в 00-03).
  • Сгенерированный код коммитим: он ревьюится в diff'е и не требует запуска sqlc на чужой машине.

Что забрать с собой

  • sqlc — кодогенератор, а не ORM: ты пишешь SQL руками, он лишь убирает ручной rows.Scan и даёт типобезопасные методы. SQL остаётся в центре.
  • Аннотация -- name: X :many|:one|:exec задаёт имя метода и форму результата; $1 типизируется и именуется из схемы (category string).
  • Ошибка в колонках/типах ловится компилятором после make gen, а не в рантайме у пользователя — это и есть выигрыш над ручным разбором из 00-04.
  • Сгенерированный internal/db/ коммитим; make gen воспроизводим (повторный прогон не даёт diff).

Дальше — юнит 00-06 «жизненный цикл соединения и пулинг»: мы пользовались пулом как чёрным ящиком (pg.NewPool), а теперь заглянем внутрь — сколько соединений он реально держит, когда открывает и закрывает их, и как увидеть свои бэкенды в pg_stat_activity.

·Модуль 01

Этот урок ещё впереди

Курс изучается по порядку — чтобы открыть этот шаг, сначала завершите предыдущие. Так контекст накапливается без пропусков.

/ вы пытались открыть
Подключение и ориентация / Типизированные запросы через sqlc