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

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

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

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

Организация кода: queries и models

Тема урока — организация кода sqlc: sqlc структура проекта, которая выдерживает не туториал на пять минут, а годы жизни репозитория. Второй урок собрал ваш первый sqlc.yaml, семнадцатый развёл домены по записям списка sql, восемнадцатый и девятнадцатый показали MySQL и SQLite рядом с PostgreSQL — и к этому моменту у трекера задач накопились вопросы, на которые «просто запусти sqlc generate» уже не отвечает. Куда класть файлы схемы и запросов, чтобы через полгода не пришлось искать? Из каких деталей складывается сгенерированный пакет — и можно ли договориться с генератором об их форме? Как делить запросы между файлами и пакетами так, чтобы каждый находил своё место? И главное — как жить с генератом ежедневно: оборачивать ли его в репозиторий, коммитить ли в git, и что делать, если в одном проекте три движка сразу. Урок отвечает на шесть вопросов. Первый: каноничная раскладка репозитория — где живут миграции, пакеты queries и сгенерированный код. Второй: анатомия пакета — db.go, models.go, querier.go и файлы-однофамильцы ваших .sql. Третий: emit-ключи и query_parameter_limit — рычаги, которыми форма генерата подгоняется под ваш стиль. Четвёртый: переиспользование — репозиторий поверх Queries, который не превращается во второй ORM. Пятый: три движка в одном репозитории без войны диалектов. Шестой: спор о генерате в git, который в двадцать втором уроке закроет CI. План курса — на [странице курса sqlc](/courses/sqlc/); разветвление на домены — [в семнадцатом уроке](/courses/sqlc/lessons/17-schemy/), а соседний рассказ про тесты — в двадцать первом.

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

Репозиторий, в котором sqlc живёт долго#

Туториал из третьего урока оставил вам плоскую папку: schema.sql, query.sql, один пакет. Для демо хватает, для продукта — нет: через полгода в одной папке встретятся схема на тысячу строк, сорок файлов запросов и попытка понять, что из этого актуально. Зрелая sqlc структура проекта опирается на три решения, и все три вы уже видели по кусочкам. Первое: схема живёт в папке миграций — шестнадцатый урок отвёл db/migrations под пары up/down у goose и golang-migrate, и sqlc читает эту же папку как источник DDL. Одна папка — один источник правды: запросы компилируются против того же SQL, который накатывается на базу. Второе: запросы живут отдельно, в пакетах queries — по домену на папку, как учил семнадцатый урок: db/queries/todos, db/queries/users. Третье: сгенерированный код уезжает в internal/store/<pkg> — не в корень, не в cmd, не рядом с обработчиками. Префикс internal работает на вас: за пределы модуля пакет импортировать нельзя, а значит, никто из чужих репозиториев случайно не начнёт зависеть от вашего генерата.

Отдельное слово про sqlc.yaml. По умолчанию команда ищет его в текущей папке, и для монорепозитория с несколькими сервисами это повод держать конфиг в корне — sqlc generate из корня соберёт все записи списка sql разом. Если сервисов много и генерация у каждого своя, конфиг можно положить рядом с сервисом и звать с флагом -f, но приберегите это для дня, когда одна запись действительно начнёт мешать другой; до того момента один файл в корне — самая читаемая организация кода sqlc, какая бывает.

Канон урока: раскладка и конфиг трекера

GO
# sqlc.yaml — в корне репозитория трекера
version: "2"
sql:
  - engine: "postgresql"
    schema: "db/migrations"          # та же папка, что накатывает migrate
    queries:                          # список путей — не только один каталог
      - "db/queries/todos"
      - "db/queries/users"
    gen:
      go:
        package: "pgdb"
        out: "internal/store/pgdb"
        sql_package: "pgx/v5"
        emit_interface: true          # Querier — шов для тестов
        emit_json_tags: true
        emit_empty_slices: true

# Дерево репозитория:
# db/
#   migrations/        001_init.sql, 002_soft_delete.sql, ...
#   queries/
#     todos/queries.sql
#     users/queries.sql
# internal/
#   store/
#     pgdb/            <- сгенерировано, руками не трогаем
#     store.go         <- наш код: репозиторий поверх Queries

Три решения раскладки в одном файле. schema указывает на папку миграций — генератор прочтёт все .sql по порядку, как в шестнадцатом уроке. queries стал списком: каталоги доменов перечислены явно, и новый домен добавляется строкой, а не перекроем старой записи. out уводит генерат в internal/store/pgdb — под защиту internal. emit-ключи здесь три, полный их разбор — таблицей ниже.

Анатомия пакета: четыре файла, из которых всё состоит#

Откройте internal/store/pgdb после генерации — и вы увидите ровно четыре сорта файлов, какие бы таблицы ни завели. Файл db.go — каркас: интерфейс DBTX с методами QueryContext, QueryRowContext, ExecContext (для database/sql ещё и PrepareContext), структура Queries, хранящая соединение, конструктор New(db DBTX) и метод WithTx, который одиннадцатый урок делал главной дверью в транзакции. Этот файл одинаков почти дословно у всех контуров курса — меняются только сигнатуры под pgx или database/sql, как видел девятнадцатый.

Файл models.go — словарь: по структуре на таблицу, имена превращает простое правило — таблица todos даёт структуру Todo, срез хвостового «s» снимается; ключ emit_exact_table_names со значением true отключает это и оставит Authors для таблицы authors. Типы полей приходят из карты типов вашего движка — та самая, что читали четвёртый, восемнадцатый и девятнадцатый уроки.

Файлы-однофамильцы запросов: todos.sql.go рядом с вашим todos/queries.sql — константа с SQL, Params-структура, если параметров больше лимита, Row-структура для выборок с джойнами из двенадцатого урока и сами методы CreateTodo, ListTodos. Имя .sql.go наследуется от имени исходного файла — вот почему нарезку по файлам стоит делать осмысленной, а не «всё в один dump.sql». Наконец, querier.go — интерфейс со всеми методами пакета, появляется при emit_interface: true; одиннадцатый урок уже читал его применительно к транзакциям.

Сгенерированный пакет глазами файловой системы

GO
internal/store/pgdb/
├── db.go            # DBTX, Queries, New, WithTx — каркас пакета
├── models.go        # Todo, User — структуры по таблицам схемы
├── querier.go       # интерфейс всех методов (emit_interface: true)
├── todos.sql.go     # методы из db/queries/todos/queries.sql
│                    #   константа createTodo, CreateTodoParams, метод CreateTodo
└── users.sql.go     # методы из db/queries/users/queries.sql

Карта соответствий, которую стоит запомнить: один файл запросов — один .sql.go, одна таблица — одна структура в models.go, весь пакет — один интерфейс в querier.go. Порядок файлов в пакетах queries ни на что не влияет: генератор сортирует сам, поэтому diff в git остаётся стабильным между запусками — генерация детерминирована, и на этом держится вся проверка в CI из двадцать второго урока.

Проверка: где искать метод

Запрос -- name: ListUsers :many лежит в файле db/queries/users/queries.sql. В каком файле сгенерированного пакета окажется метод ListUsers?

emit-ключи: рычаги формы генерата#

Генератор настраивается не флагами командной строки, а ключами в gen.go записи — вторая серия их мелькала ещё во втором уроке, здесь самое время собрать карту целиком.emit_interface включает querier.go — интерфейс над всеми методами; для организации кода sqlc это главный шов: сервис будет зависеть от интерфейса, а не от структуры, и тесты из двадцать первого урока подменят реализацию без докера. emit_json_tags добавляет теги json к структурам моделей; удобно для маленьких сервисов, где модель таблицы сразу уезжает в ответ API, и спорно для больших, где слои стоит разводить. emit_pointers_for_null_types меняет представление nullable-колонок: вместо sql.NullString — *string; девятый урок разбирал эту развилку как вопрос вкуса и кодовой базы. emit_empty_slices заставляет методы :many возвращать пустой срез вместо nil — мелочь, которая убирает целый класс «if rows == nil» в обработчиках.

Дальше — ключи, управляющие именами и объёмом. emit_exact_table_names уже разобрали: имена структур без сингуляризации. omit_unused_structs режет из models.go структуры таблиц, на которые не ссылается ни один запрос — осмысленно в монорепозитории, где один конфиг собирает несколько пакетов, как в семнадцатом. query_parameter_limit задаёт порог: пока параметров не больше N (по умолчанию один), они идут в сигнатуру метода по отдельности, как только больше — sqlc упаковывает их в структуру <Query>Params. Ключ rename в gen.go переназначает имена колонок, конфликтующие с ключевыми словами или стилем: колонка order станет Order по вашему указанию, а не сломает генерацию.

Карта gen.go: ключ → что меняет#

КлючЧто делаетКогда включать
emit_interface: trueгенерирует querier.go — интерфейс всех методовпочти всегда: шов для тестов и подмен
emit_json_tags: trueтеги json у полей структурмаленькие API без отдельных DTO
emit_pointers_for_null_types: truenullable-колонки как T вместо sql.NullTкогда указатели понятнее обёрток (урок 09)
emit_empty_slices: true:many возвращает [] вместо nilчтобы не проверять срезы на nil
emit_exact_table_names: trueструктура Authors вместо Authorкогда сингуляризация врёт про домен
emit_methods_with_db_argument: trueметоды принимают DBTX аргументомобщий Queries для базы и транзакций
omit_unused_structs: trueрежет модели без ссылок из запросовмного записей sql в одном конфиге
query_parameter_limit: Nпорог появления <Query>Paramsхотите структуры даже для 1-2 параметров
output_db_file_name и соседипереименовать db.go/models.go/querier.goконфликты имён в общем каталоге
rename (gen.go)карта колонка → имя поля Goколонки-конфликты вроде order, user

Предскажите сигнатуру

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

# sqlc.yaml (gen.go)
gen:
  go:
    package: "pgdb"
    out: "internal/store/pgdb"
    sql_package: "pgx/v5"
    query_parameter_limit: 1

-- db/queries/todos/queries.sql
-- name: RenameTodo :exec
UPDATE todos SET title = $1 WHERE id = $2 AND user_id = $3;

Пакеты queries: нарезка, имена и что переиспользуется#

Запросы — это единственный код, который вы пишете руками, поэтому вся дисциплина организации кода sqlc сходится к тому, как вы держите пакеты queries. Правило нарезки: папка — домен, файл — тема внутри домена. В todos лежат queries.sql с крудом и stats.sql с агрегациями из тринадцатого урока — и генерат раскладывается по файлам-близнецам, как вы уже знаете. Имена запросов делайте глагол+сущность: CreateTodo, ListUsers, CompleteTodo — они станут именами методов, и от этого зависит читаемость вызовов по всему проекту. Дубликат имени внутри одной записи sql — ошибка генерации: два CreateTodo в разных файлах одного пакета не соберутся, потому что в Go не может быть двух методов с одним именем на приёмнике.

Чего у sqlc нет — include-ов и макросов общего SQL: если пять запросов используют одно условие, придётся либо написать его пять раз, либо вынести его в базу как представление, либо собрать запрос на стороне Go из констант. Это осознанный минимализм: sqlc компилирует то, что видит, и не тащит препроцессор. Зато повторное использование параметров уже в языке запросов — восьмой урок показывал, как sqlc.arg(name) при повторном упоминании даёт один параметр, а не два. И не забывайте про sqlc.narg из девятого: nullable-параметры в UPDATE — половина «динамических» фильтров из пятнадцатого урока живёт именно на них, без всякой магии конкатенации.

Два файла одного домена

GO
-- db/queries/todos/queries.sql: круд домена
-- name: CreateTodo :one
INSERT INTO todos (user_id, title, priority)
VALUES ($1, $2, $3)
RETURNING *;

-- name: ListTodosByUser :many
SELECT * FROM todos WHERE user_id = $1 ORDER BY created_at DESC;

-- db/queries/todos/stats.sql: аналитика того же домена
-- name: CountOpenTodos :one
SELECT count(*) FROM todos WHERE done = false;

-- name: OpenTodosByPriority :many
SELECT priority, count(*) AS cnt FROM todos
WHERE done = false GROUP BY priority ORDER BY priority;

Два файла — два .sql.go в генерате, и это удобно для навигации: код круда ищется в queries.sql.go, аналитика — в stats.sql.go. Имена — глагол плюс сущность: CreateTodo, ListTodosByUser, CountOpenTodos станут методами, которые читаются как предложения. Обратите внимание: оба файла живут в одной записи конфига, поэтому попадают в один пакет pgdb — отдельная запись sql, как в семнадцатом уроке, понадобилась бы только если бы домен требовал свой out или свои overrides.

Переиспользование: репозиторий поверх Queries#

Самый частый вопрос после анатомии — использовать ли сгенерированный пакет напрямую из обработчиков. Для маленького сервиса — да: pgdb.New(pool) в main, Queries в зависимостях хендлеров, и всё честно. Но в проекте, который растёт, есть две причины обернуть генерат в свой тонкий слой. Первая — транзакции: метод домена вроде «закрыть задачу и списать деньги» должен выполняться атомарно, и одиннадцатый урок строил для этого хелпер withTx; если каждый обработчик сам открывает транзакцию, правила расползаются. Вторая — тесты: сервис, зависящий от интерфейса, проверяется фейком без базы, а интеграционные тесты из двадцать первого урока достанутся только обёртке.

Классический паттерн — структура-обёртка, которая встраивает Queries и добавляет свою транзакционную дверь. Встраивание даёт красивую механику: все методы Queries становятся методами Store как есть, при этом Store может добавлять свои. Метод ExecTx у обёртки принимает функцию и передаёт ей Queries, привязанную к транзакции через WithTx, — тот самый контракт одиннадцатого урока, но спрятанный за одной точкой входа. Границу не переступайте: обёртка без логики, без кэшей, без «умных» конвертеров — иначе вы построите второй ORM поверх генератора, который от ORM как раз уходил. Правило: Store — это про время жизни соединений и транзакций, а не про бизнес-правила.

Store: встраивание Queries и одна дверь в транзакции

GO
// internal/store/store.go — наш код рядом с генератом
package store

import (
        "context"

        "github.com/jackc/pgx/v5/pgxpool"

        "tracker/internal/store/pgdb"
)

type Store struct {
        *pgdb.Queries // встраивание: методы Queries — теперь методы Store
        pool *pgxpool.Pool
}

func New(pool *pgxpool.Pool) *Store {
        return &Store{Queries: pgdb.New(pool), pool: pool}
}

// ExecTx — единственная дверь в транзакцию: контракт урока 11
func (s *Store) ExecTx(ctx context.Context, fn func(*pgdb.Queries) error) error {
        tx, err := s.pool.Begin(ctx)
        if err != nil {
                return err
        }
        defer tx.Rollback(ctx)

        if err := fn(s.Queries.WithTx(tx)); err != nil {
                return err
        }
        return tx.Commit(ctx)
}

Три строки, на которых держится паттерн. Встраивание pgdb.Queries делает CreateTodo доступным как s.CreateTodo — обработчики не знают про pgdb.New. В ExecTx хелпер withTx из одиннадцатого урока превратился в метод: Begin, defer Rollback, вызов fn с Queries, привязанным к tx через WithTx, Commit на успехе. Вызов из домена: store.ExecTx(ctx, func(q pgdb.Queries) error { ... }) — внутри функции доступны и запросы, и транзакционность, и ни один обработчик не видит Begin руками.

Проверка: транзакция через обёртку

Store встраивает *pgdb.Queries. Как правильно выполнить CreateTodo и LedgerInsert в одной транзакции через Store?

Три движка в одном репозитории#

Девятнадцатый урок закончился обещанием: конфиг, где postgresql, mysql и sqlite живут по соседству. Теперь у вас есть все детали, чтобы его выполнить. Каждая запись списка sql — свой пакет со своим out, это механика семнадцатого. Схема у движков общая по смыслу, но не по синтаксису: SERIAL против INTEGER PRIMARY KEY, TIMESTAMPTZ против TEXT — поэтому схема разделяется на db/schema/pg, db/schema/my, db/schema/lite, и тут же встаёт вопрос о пакетах queries: дублировать ли SQL трижды?

Честный ответ зависит от того, насколько диалекты расходятся. Если запросы — фильтры, вставки, обновления на голом SQL без экзотики, один каталог db/queries обслуживает все три записи: массив queries может указывать на одну и ту же папку из разных записей, а плейсхолдеры ? и $1 останутся единственным расходящимся штрихом, и sqlc молча переварит оба стиля в своём парсере. Как только появляются массивы ANY($1::int[]), ON CONFLICT, RETURNING с особенностями — выносите специфичное в db/queries/pg и указывайте его только в PG-записи: общий каталог минус папка специфики. Помните и о картах типов из восемнадцатого и девятнадцатого уроков: DECIMAL в MySQL — string, в SQLite — float64, то есть одна и та же колонка даёт разные поля в соседних пакетах, и обёртка Store — правильное место, чтобы разницу проглотить. Трекер из курса живёт на PostgreSQL, а SQLite-пакет обслуживает тесты и CLI — это ровно та модель, где мультидвижковость не превращается в налог.

Конфиг мультидвижкового репозитория

GO
# sqlc.yaml — три записи, три пакета
version: "2"
sql:
  - engine: "postgresql"
    schema: "db/schema/pg"
    queries: ["db/queries/common", "db/queries/pg"]
    gen:
      go:
        package: "pgdb"
        out: "internal/store/pgdb"
        sql_package: "pgx/v5"

  - engine: "mysql"
    schema: "db/schema/my"
    queries: ["db/queries/common"]
    gen:
      go:
        package: "mydb"
        out: "internal/store/mydb"

  - engine: "sqlite"
    schema: "db/schema/lite"
    queries: ["db/queries/common"]
    gen:
      go:
        package: "sqlitedb"
        out: "internal/store/sqlitedb"

Один запуск sqlc generate собирает три пакета: pgdb — полный, с доменной спецификой из db/queries/pg; mydb и sqlitedb — общий набор из db/queries/common. Схемы разнесены по папкам движков, потому что синтаксис DDL расходится раньше, чем запросы. Каждая запись компилируется независимо: ошибка в mysql-запросе не помешает сборке pgdb. Такие же три пакета вы собирали по одному в восемнадцатом и девятнадцатом — здесь они просто живут под одной крышей.

Впишите ключи организации пакета

Заполните пропуски: ключ интерфейса методов, ключ тегов json, порог Params-структур и каталог, в который попадает генерат.

# sqlc.yaml version: "2" sql: - engine: "postgresql" schema: "db/migrations" queries: "db/queries" gen: go: package: "pgdb" out: "internal/store/pgdb" : true # querier.go: интерфейс всех методов : true # теги json у структур : 1 # больше одного параметра — структура <Query>Params

Коммитить генерат или генерировать на месте#

Последнее организационное решение — судьба сгенерированных файлов в git, и оно делит сообщество надвое. Аргументы за коммит: пакет собирается у любого, кто склонировал репозиторий, без установки sqlc; diff генерата в pull request показывает ровно то, что изменит запрос — ревьюер видит новые поля структур и новые методы; версии sqlc у всех команды и в деплое совпадают по умолчанию. Аргументы против: PR шумит сгенерированными строками, а если кто-то забудет перегенерировать, репозиторий уезжает в дрейф — источники говорят одно, генерат другое.

Дрейф лечится механикой, а не запретами:sqlc diff из двадцать второго урока сравнит генерат с тем, что выдал бы текущий конфиг, и уронит CI при расхождении. Практика, которая работает в большинстве команд: коммитим генерат, CI проверяет diff, версия sqlc зафиксирована. Фиксация делается прямо в go:generate-директиве: go run с явной версией модуля тянет именно её, и генерация перестаёт зависеть от того, что установлено у разработчика в PATH. Экономия на коммите генерата (репозиторий тоньше, diff чище) почти всегда оборачивается tax-ом на сборку: каждому потребителю нужен установленный sqlc, а откат релиза — это не revert-коммит, а повторная генерация на месте. Оставьте «генерим на месте» проектам, где слой данных меняется каждый день, и вернитесь к нему, когда CI из двадцать второго урока научится проверять всё сам.

go:generate: версия генератора в коде

GO
// tools/generate.go — директива с зафиксированной версией
package tools

//go:generate go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.27.0 generate

// Запуск: go generate ./tools
// Второй путь — бинарник в PATH:
//   sqlc generate   (версию держим одинаковой через CI и README)

Директива go:generate превращает генерацию в команду go generate ./... — одну для всего репозитория, вместе с ent-проектами или protobuf-задачами, если появятся. Явная версия в пути модуля (@v1.27.0) — это пиннинг: у всех разработчиков и в CI сгенерируется одинаковый код, и diff-проверка не будет падать из-за разницы версий инструмента. Альтернатива — бинарник sqlc в PATH: быстрее, но версию придётся синхронизировать вне git.

Соберите переезд на каноничную раскладку

Расставьте шаги перевода проекта на sqlc структуру проекта из урока.

Строки

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

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

Пять правил организации кода sqlc#

  • Схема в db/migrations, запросы в db/queries/<домен>, генерат в internal/store/<pkg> — три места, которые ищутся без grep.
  • Один файл запросов — один .sql.go: имя файла — часть публичного устройства пакета.
  • Имена запросов — глагол плюс сущность: они становятся методами, которые вы читаете каждый день.
  • Генерат не редактируется: все правки через sqlc.yaml, overrides, rename и SQL-файлы.
  • Обёртка Store — про соединения и транзакции, а не про бизнес-логику; иначе это второй ORM.

Что дальше: структура, которая готова к тестам#

Оглядитесь: раскладка стоит, пакет собран из четырёх сортов файлов, emit-карта настроила форму, Store закрыл транзакции, а мультидвижковый конфиг держит три мира в одном репозитории. Если запрос «sqlc организация кода» сворачивать в один ответ, ответом будет эта раскладка плюс договорённости вокруг генерата — всё остальное в курсе оказалось её следствиями. Главная причина, по которой всё это делалось именно так, а не «лишь бы компилировалось» — тесты. Интерфейс querier.go — это место, куда сервис может встать на учет и не звать базу в юнит-тестах; DBTX — шов, через который генерат равнодушен к тому, живая база перед ним, sqlite в памяти или мок; Store с ExecTx — единственная точка, которую нужно проверять на атомарность. Двадцать первый урок разложит это хозяйство по пирамиде: юнит-тесты на фейках репозитория, интеграционные на sqlite из девятнадцатого и на настоящем Postgres, sqlmock — и покажет, где моки врут, а где честнее самой базы. Если вы ещё не включили emit_interface — сделайте это сегодня: завтра будет поздно, потому что сервисы уже встанут на конкретную структуру, а не на интерфейс.

Карточки: организация кода

Термин

Каноничная раскладка sqlc-репозитория?

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

db/migrations — схема (тот же DDL, что накатывают миграции), db/queries/<домен> — пакеты queries, internal/store/<pkg> — генерат под защитой internal, sqlc.yaml в корне

1 / 6

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

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

Вывод

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

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

  • sqlc структура проекта: db/migrations для схемы, db/queries/<домен> для пакетов queries, internal/store/<pkg> для генерата, sqlc.yaml в корне; queries и schema принимают списки путей.
  • Сгенерированный пакет — четыре сорта файлов: db.go (DBTX, Queries, New, WithTx), models.go (структуры таблиц), <файл>.sql.go (методы по одному на файл запросов), querier.go (интерфейс при emit_interface).
  • Форму генерата правят ключи gen.go: emit_interface, emit_json_tags, emit_pointers_for_null_types, emit_empty_slices, emit_exact_table_names, omit_unused_structs, query_parameter_limit, rename.
  • Переиспользование: обёртка Store встраивает *Queries и даёт ExecTx (Begin, defer Rollback, fn с WithTx, Commit) — транзакционная дверь одиннадцатого урока в одном месте.
  • Три движка в одном репозитории: запись списка sql на пакет, схемы по папкам движков, общие запросы в db/queries/common, специфика — в db/queries/pg; карты типов движков различают одни и те же колонки.
  • Генерат коммитим и не правим руками; версию sqlc пиним в go:generate (go run ...@v1.27.0); дрейф ловит sqlc diff — тема двадцать второго урока.

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

Первое: переведите трекер на раскладку урока — db/queries/todos и db/queries/users с двумя файлами в первом домене, out в internal/store/pgdb, queries списком каталогов; убедитесь, что в пакете появились todos.sql.go и users.sql.go, и что имена файлов совпали с вашими. Второе: включите emit_interface и напишите в cmd маленькую функцию, принимающую pgdb.Querier, — вызовите её с *pgdb.Queries и убедитесь, что компилятор доволен. Третье: поставьте query_parameter_limit: 1 и найдите в генерате запрос, у которого Params-структура появилась после смены ключа, — сравните сигнатуры до и после. Четвёртое: соберите Store с ExecTx, закройте две вставки в одной транзакции и проверьте откат: верните ошибку из fn и убедитесь, что ни одна строка не записалась. Пятое: добавьте sqlite-запись в тот же конфиг по образцу девятнадцатого урока, укажите общий каталог queries и одну PG-специфичную папку — посмотрите, какие запросы действительно собираются на обоих движках, а какие требуют выноса. Шестое: попробуйте отредактировать *.sql.go руками, запустите sqlc generate и опишите письменно, что произошло, — эта шпаргалка пригодится, когда соблазн вернётся. Седьмое: добавьте go:generate-директиву с пином версии и проверьте, что go generate ./... даёт diff, идентичный вашему бинарнику sqlc той же версии.

Личные заметкиПусто
Хранятся только в вашем браузере (sqlc · Организация кода: queries и models)

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

Можно ли держать один каталог queries для трёх движков сразу?

Да, пока запросы не используют специфичный диалект: массив queries в разных записях sql может указывать на одну и ту же папку, и каждый движок скомпилирует её своей картой типов. Как только появляются PG-массивы, ON CONFLICT или SQLite-праги в запросах, специфичное выносится в отдельную папку (db/queries/pg) и подключается только в соответствующей записи. Схема при этом почти всегда разделяется: DDL у движков расходится раньше запросов.

Где должен лежать sqlc.yaml — в корне или рядом с сервисом?

В корне, пока сервис один: команда sqlc generate без аргументов находит его в текущей папке, и вся генерация репозитория — одна команда. Несколько конфигов оправданы в монорепозитории, где сервисы независимы и генерация одного не должна триггерить другой; в этом случае конфиг кладут рядом с сервисом и зовут sqlc generate -f path/to/sqlc.yaml. Промежуточный вариант — один конфиг с несколькими записями sql, как в семнадцатом и этом уроке.

Почему не стоит оборачивать Queries в толстый репозиторий с логикой?

Потому что вы получите ORM, от которого sqlc вас освободил, только написанный своими руками и без документации. Сгенерированный код уже даёт типобезопасные методы с честными структурами; обёртка оправдана ровно в двух ролях — управление соединениями и транзакциями (ExecTx) и изоляция интерфейса для тестов. Конвертеры бизнес-объектов, кэши и правила валидации живут выше, в доменном слое, где они проверяются юнит-тестами без базы.

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

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

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

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

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

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

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

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

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