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

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

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

Урок 1 из 25 ~15 мин +50 XP

Знакомство с sqlc: почему SQL-first

Представьте: в пятницу вечером падает эндпоинт с ошибкой `column "crated_at" does not exist` — опечатка внутри строки условия дожила до продакшена, потому что компилятор Go не заглядывает внутрь SQL-строк. Что такое sqlc: консольный генератор, который выносит SQL из строк в .sql-файлы, проверяет запросы по схеме базы и пишет за вас типобезопасный Go-код вместо ручного Scan. Разберём философию SQL-first, сравним sqlc vs GORM по этапу ловли ошибок и честно определим, когда sqlc — лучший выбор, а когда проще ORM.

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

Сценарий, знакомый каждому, кто писал бэкенд на Go. Пятница, вечер, в продакшене падает эндпоинт с сообщением column "crated_at" does not exist. Днём кто-то переименовал колонку в базе и поправил основной код — а одно условие, спрятанное строкой внутри вызова Where(...), проверять было нечем: компилятор внутрь строк не смотрит, тест на этой ветке не гонялся. Код собрался, всё выглядело зелёным — до первого запроса.

Это история не про кривые руки, а про устройство категории инструментов: SQL, записанный строкой в Go-коде, невидим ни для компилятора, ни для рефакторингов, ни для автодополнения. Чем больше сервис, тем больше таких строк и тем дороже каждая. В этом уроке мы разберём sqlc — инструмент, который выносит SQL из строк в отдельные файлы, проверяет его по схеме базы и генерирует вместо вас всю нудную Go-обвязку. Связка «SQL-first плюс кодогенерация» — и есть ответ на вопрос, почему sqlc вообще появился.

SQL в строках: что видит и чего не видит компилятор

GO
// GORM: условие — обычная строка
db.Where("user_id = ? AND crated_at > ?", uid, since).Find(&todos)

// database/sql: запрос — тоже строка, плюс ручной Scan по колонкам
rows, err := db.Query("SELECT id, titel FROM todos WHERE user_id = $1", uid)

Обе строки прекрасно компилируются: для Go это просто текст. Опечатка crated_at выстрелит в рантайме сообщением от драйвера, titel — тем же способом, только позже и, возможно, у пользователя. Линтеры и автодополнение сюда не заглядывают: строка есть строка. В среднем сервисе таких строк сотни. sqlc сделан ровно для того, чтобы вытащить их на свет — в .sql-файлы, где их можно проверить по схеме и превратить в типизированные функции.

Что такое sqlc: генератор, а не библиотека#

Что такое sqlc, проще всего объяснить через то, чем он не является. Это не ORM и не драйвер: в рантайме sqlc вообще не участвует в работе сервиса. Это консольный инструмент, который читает два входа — схему таблиц и аннотированные SQL-запросы в .sql-файлах — и пишет на выходе обычный Go-код: структуры для таблиц, методы для каждого запроса, аккуратный Scan внутри каждого метода. Рефлексии и struct-тегов в сгенерированном коде нет: это те же строки, которые вы писали бы руками, только без ошибок и опечаток.

Проект начал Кайл Конрой в 2019 году, в 2020-м sqlc дорос до версии 1.0 и с тех пор стал де-факто стандартом SQL-first-подхода в Go. Ставится одной командой: go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest, есть и brew. Команд по сути три: sqlc init создаёт заготовку конфига sqlc.yaml, sqlc generate запускает генерацию, sqlc vet проверяет запросы правилами — до последней доберёмся в конце курса. Настройке конфига посвящён второй урок, сегодня важно само устройство конвейера.

Философия SQL-first: источник правды — база#

ORM переворачивает привычную работу с данными: источник правды — структура Go, а SQL библиотека сочиняет сама. sqlc ставит всё с ног на голову. Вы пишете SQL — такой, каким он уйдёт в базу: с джойнами, индексными условиями, оконными функциями, если они нужны. Гарантия «запрос делает ровно то, что написано» здесь бесплатна: вы видите текст целиком, а не догадываетесь, во что развернётся цепочка методов. Для человека, который дружит с SQL, это не ограничение, а облегчение: JOIN руками — не наказание, а десять строк прозрачного текста.

Отсюда и главная характеристика курса: sqlc — инструмент для тех, кто SQL уже знает. Синтаксис он не учит и плохой запрос не починит — просто честно сгенерирует код под него. Если SQL пока даётся с трудом, честный совет: начните с курса GORM, где слой данных спрятан за библиотекой, а сюда возвращайтесь, когда захочется контроля над каждым запросом.

Вход 1: схема

GO
-- schema.sql
CREATE TABLE todos (
  id    BIGSERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  done  BOOLEAN NOT NULL DEFAULT false
);

Схема — отправная точка: без неё sqlc не знает, какие колонки существуют и какие у них типы. Это может быть один файл, как здесь, или папка с миграциями golang-migrate и goose — sqlc прочитает файлы в порядке имён и соберёт итоговую схему. Годится даже живая база, из которой схему можно вытащить напрямую — такой путь разбираем в уроках ближе к концу курса. В примерах всего курса возим таблицу todos: задачи трекера, до которого в итоговом проекте дойдём целиком.

Вход 2: запросы с аннотациями

GO
-- query.sql

-- name: GetTodo :one
SELECT * FROM todos WHERE id = $1 LIMIT 1;

-- name: ListOpenTodos :many
SELECT * FROM todos WHERE done = false ORDER BY id;

-- name: CreateTodo :exec
INSERT INTO todos (title) VALUES ($1);

Перед каждым запросом — аннотация: имя будущей функции и форма возврата. :one вернёт одну строку (Todo, error), :many — срез ([]Todo, error), :exec — только error, для INSERT без возврата. Есть и другие формы: :execrows отдаёт число затронутых строк, :execresult — sql.Result, а для pgx есть :copyfrom и пакетные формы :batch. Знак $1 — плейсхолдер PostgreSQL; в MySQL и SQLite пишется вопросом. Звёздочку в SELECT sqlc раскроет в явный список колонок — как это делает сама база.

Выход: типобезопасный Go-код

GO
// models.go — структура по таблице
type Todo struct {
        ID    int64
        Title string
        Done  bool
}

// query.sql.go — метод по запросу (сокращено)
const getTodo = `-- name: GetTodo :one
SELECT id, title, done FROM todos WHERE id = $1 LIMIT 1`

func (q *Queries) GetTodo(ctx context.Context, id int64) (Todo, error) {
        row := q.db.QueryRowContext(ctx, getTodo, id)
        var i Todo
        err := row.Scan(&i.ID, &i.Title, &i.Done)
        return i, err
}

BIGSERIAL превратился в int64, TEXT — в string, BOOLEAN — в bool. Nullable-колонки получили бы sql.NullString, указатели или типы pgtype — этому посвящён девятый урок. GetTodo принимает контекст и типизированный id int64: передать строку вместо числа или перепутать порядок аргументов не получится — не пустит компилятор. Внутри метода — тот самый Scan, который раньше вы писали руками. Сгенерированный код можно открыть и прочитать целиком: sqlc ничего не прячет, рядом лежит db.go с интерфейсом DBTX и методом WithTx для транзакций.

Как этим пользоваться

GO
conn, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
        return err
}
defer conn.Close()

queries := db.New(conn) // db — сгенерированный пакет

todo, err := queries.GetTodo(ctx, 42)
if err != nil {
        return fmt.Errorf("получить задачу: %w", err)
}
fmt.Println(todo.Title) // автодополнение знает поля

db.New оборачивает соединение — *sql.DB или пул pgx — в структуру Queries. Дальше обычный Go: err, todo.Title, свои обёртки над ошибками. Если под условие не попало ни одной строки, вернётся sql.ErrNoRows, знакомый по database/sql. Так выглядит весь контракт sqlc: до генерации — SQL и схема, после — обычный код, который не отличить от рукописного.

Что sqlc делает за вас#

  • Проверяет каждый запрос по схеме: таблицы, колонки, типы — до компиляции и тем более до продакшена
  • Генерирует структуры таблиц в models.go и по одному файлу на каждый .sql с запросами
  • Превращает плейсхолдеры в типизированные аргументы функции: $1 с типом bigint становится id int64
  • Пишет Scan за вас: строки результата раскладываются по полям структур
  • Работает с database/sql и с pgx/v4, pgx/v5 — переключается одной строкой в конфиге
  • Держит транзакции на методе WithTx, который принимает *sql.Tx или pgx.Tx (урок 11)

sqlc vs gorm: где ловится ошибка#

Сравнение sqlc vs gorm удобнее всего вести по одной оси: на каком этапе проявится ошибка в слое данных. Возьмём переименование колонки. В GORM строка условия и поля структуры расходятся молча: компилятор всё устраивает, библиотека в рантайме отправит запрос со старым именем — и вы узнаете об этом от драйвера, лучше всего в пятницу вечером. У sqlc цикл другой: правите схему, запускаете sqlc generate — и если запрос разъехался со схемой, генерация падает с указанием файла и строки. А если запросы перегенерировались, но вызовы вы обновить забыли, их отсечёт уже компилятор Go: сигнатуры-то изменились. Двойной заслон вместо одного.

Оборотная сторона: GORM строит запросы в рантайме и потому умеет менять их на лету, sqlc работает только с тем, что зафиксировано в файлах. Так что sqlc — не «лучший GORM», а другой контракт работы с базой; выбирать нужно под тип задач, и следующая таблица в этом поможет.

sqlc vs GORM: сравнение по критериям#

КритерийsqlcGORM
Что пишет разработчикSQL в .sql-файлахструктуры и цепочки методов
Кто собирает SQLвы сами, рукамибиблиотека в рантайме
Ошибка в имени колонкина sqlc generate, до компиляциив рантайме
Рефлексия и struct-тегинет — обычный Go-кодоснова всей библиотеки
Динамические запросытрудно: COALESCE, отдельные выборкилегко: Where() с условиями
Миграциинет, снаружи (golang-migrate, goose)AutoMigrate из коробки
Что нужно знатьуверенный SQLAPI библиотеки

Когда sqlc — не лучший выбор#

Обещал честность — вот мой случай. Первый проект, который я тащил на sqlc, начинался со страницы поиска: шесть опциональных фильтров, любая комбинация, сортировка на выбор. В GORM это три строки с Where(), в sqlc — полдень возни: либо один запрос с COALESCE-трюками, либо отдельная выборка под каждую комбинацию. Динамический SQL sqlc сознательно не умеет, и после того дня я знаю, где у него проходит граница.

Моё правило с тех пор такое: если схема ещё не устоялась и модель данных меняется каждую неделю, sqlc будет мешать — каждая правка колонки означает перегенерацию и волну ошибок компиляции. Это фича, а не баг, но в прототипе она бесит, и такой проект я спокойно веду на GORM. Не берите sqlc и там, где команда не знает SQL: инструмент не спасёт от плохих запросов, он их только зафиксирует. Зато когда схема стабильна, запросов много, а половина из них — отчёты и тяжёлые выборки, sqlc окупается с первой недели: типобезопасность и читаемость там решают. Развёрнутое сравнение с GORM, ent и squirrel — в двадцать четвёртом уроке.

Проверка: что такое sqlc

Что именно делает sqlc?

Проверка: ошибку в колонке

В запросе опечатка: SELECT titel FROM todos. Что произойдёт при sqlc generate?

Прочитайте запрос как sqlc: какая сигнатура получится?

Прочитайте код и выберите, что он выведет:

-- schema.sql
CREATE TABLE todos (
  id    BIGSERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  done  BOOLEAN NOT NULL DEFAULT false
);

-- query.sql
-- name: CountOpenTodos :one
SELECT count(*) FROM todos WHERE done = false;

Восстановите аннотации

Два запроса, две формы возврата: чтение одной записи и список. Впишите формы в аннотации.

-- name: GetTodo SELECT id, title, done FROM todos WHERE id = $1 LIMIT 1; -- name: ListOpenTodos SELECT id, title, done FROM todos WHERE done = false ORDER BY id;

Соберите рабочий цикл sqlc

Расставьте шаги от чистого репозитория до вызова сгенерированного метода.

Строки

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

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

Карточки: словарь sqlc

Термин

sqlc

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

CLI-генератор кода (Кайл Конрой, 2019): читает схему и аннотированные SQL-запросы из .sql-файлов, пишет типобезопасный Go-код. В рантайме не участвует.

1 / 6

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

Во втором уроке поставим sqlc и соберём конфиг: структура sqlc.yaml, версия, движки postgresql, mysql и sqlite, вывод пакетов. В третьем — первые запросы и генерация: напишем GetAuthor-подобные выборки и посмотрим на результат целиком. Весь план из 25 уроков — на странице курса, курс ведём на сквозном примере трекера задач до итогового проекта включительно.

А если вы ещё не выбрали подход, полистайте вводный урок курса GORM — там та же работа со слоем данных показана с ORM-стороны. Держать перед глазами обе картины полезно: в двадцать четвёртом уроке будем выбирать между ними уже осознанно.

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

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • sqlc — CLI-генератор, а не библиотека: в рантайме работает только сгенерированный обычный Go-код.
  • SQL-first: запросы в .sql-файлах — источник правды, структуры и Scan генерируются из них.
  • Аннотация -- name: задаёт имя функции и форму возврата: :one, :many, :exec и другие.
  • sqlc проверяет запросы по схеме на этапе генерации — ошибка в колонке не доходит ни до компилятора, ни до продакшена.
  • Работает с database/sql и pgx/v4, pgx/v5; переключение — одна строка в sqlc.yaml.
  • Динамические запросы, миграции и связи — не зона sqlc: первое через COALESCE, второе снаружи (goose, golang-migrate).
  • sqlc силён при стабильной схеме и уверенном SQL; в прототипе с плавающей моделью удобнее ORM.

Домашнее задание

Поставьте sqlc локально (go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest), выполните sqlc init и соберите конфиг под движок sqlite (тип BIGSERIAL из урока замените на INTEGER PRIMARY KEY AUTOINCREMENT). Возьмите схему todos, добавьте колонку priority INTEGER NOT NULL DEFAULT 0, напишите три запроса с аннотациями :one, :many и :exec, запустите sqlc generate. Откройте сгенерированные файлы и найдите структуру Todo, константу с текстом запроса и метод со Scan. Затем переименуйте колонку в запросе и посмотрите на ошибку генерации — это то самое сообщение, которое в обычном проекте спасает продакшен.

Личные заметкиПусто
Хранятся только в вашем браузере (sqlc · Знакомство с sqlc: почему SQL-first)

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

Что такое sqlc простыми словами?

Программа, которая читает ваши SQL-запросы и схему базы, а затем пишет за вас обычный Go-код: структуры для таблиц и функции для запросов с готовым Scan. Вы отвечаете за SQL, sqlc — за типобезопасную обвязку.

sqlc vs GORM: что выбрать?

Зависит от задач. Стабильная схема, много отчётов, команда, знающая SQL, — берите sqlc: ошибки ловятся на генерации. Быстрый прототип, частые смены модели, AutoMigrate и ассоциации из коробки — удобнее GORM. В одном проекте их можно совмещать.

Работает ли sqlc только с PostgreSQL?

Нет. Кроме PostgreSQL есть движки MySQL и SQLite, а на стороне Go — database/sql (включая pgx в режиме stdlib) и нативные pgx/v4, pgx/v5. Движок и драйвер выбираются в sqlc.yaml.

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

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

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

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

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

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

ent: графовая ORM с кодогенерацией

Курс ent: схемы, поля, связи, запросы, хуки, privacy, миграции и GraphQL. 25 уроков с примерами и проектом микроблога.

  • Проектировать схемы и связи в ent
  • Генерировать код и жить с кодогенерацией
  • Писать запросы builder-стилем с типами
  • Строить хуки, интерсепторы и privacy-правила