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

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

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

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

Несколько схем и организация

Тема урока — sqlc схемы и организация проекта: момент, в который трекеру становится тесно в одном пакете queries. Шестнадцать уроков канон держался на минимализме: одна база tracker, одна схема public, один пакет db с одним Querier, в котором рядом живут задачи, пользователи и журнал списаний. Пока методов было два десятка, это было достоинством; но сгенерированный интерфейс вырос до экрана прокрутки, папка db/queries превратилась в плоский список файлов разной доменной принадлежности, а бухгалтерия спрашивает, почему её таблица лежит в одной куче с задачами. Урок отвечает на четыре вопроса. Первый: что такое схемы Postgres и зачем домену собственное пространство имён — CREATE SCHEMA billing и квалифицированные имена. Второй: как sqlc видит это устройство — search_path генератор не эмулирует, неквалифицированные имена ищет только в public, а таблицам вне умолчания выдаёт Go-имена с префиксом схемы. Третий: sqlc.yaml multiple — список sql, в котором каждая запись собирает собственный Go-пакет: schema, queries и gen.go на пакет. Четвёртый: как жить с моделями на стыке — дубль усекается omit_unused_structs, имена файлов настраиваются, общие типы выносятся глобальными overrides. Организация sqlc — это не про красоту каталогов, а про границы, которые компилятор умеет охранять. Конфиг, который мы разветвляем, завёлся во [втором уроке](/courses/sqlc/lessons/02-nastrojka/); папка миграций как источник схемы — в [шестнадцатом](/courses/sqlc/lessons/16-migracii/); план остальных тем — на странице [курса sqlc](/courses/sqlc/).

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

К семнадцатому уроку у трекера три таблицы и десятки методов. Задачи с их фильтрами и страницами — сердце продукта; пользователи — исполнители, с которыми задачи соединяются джойном; журнал списаний — бухгалтерский домен, куда одиннадцатый урок положил суммы за закрытые задачи. Всё это десятилетиями (то есть шестнадцатью уроками) жило в одном пакете db: одна папка db/queries с файлами по темам, один models.go, один Querier, в котором CreateSpending стоит соседом с SearchTodos. Пока доменов было ноль, а тем — много, это работало.

Потом появляются боли, каждая из которых знакома любому, кто обслуживал выросший проект. Первая: Querier — это список всех методов проекта сразу, и на ревью вопроса «кто владеет этим запросом» отвечает только комментарий. Вторая: файлы в db/queries перемешаны по доменам — spendings.sql лежит рядом с pagination.sql, и ничто, кроме конвенции, не мешает новому коллеге добавить бухгалтерский запрос в файл задач. Третья: изменения домена не видны на границе — из сигнатур не видно, что панель фильтров не имеет права трогать журнал списаний.

У Postgres для этого есть встроенный инструмент — схемы: пространства имён внутри одной базы. У sqlc — своя часть ответа: несколько записей в sqlc.yaml, каждая со своей папкой queries и собственным Go-пакетом на выходе. Урок соединяет оба уровня: сначала схема в базе и то, как генератор её читает, потом пакеты в конфиге и то, как они делят код по доменам.

sqlc схемы: домены внутри одной базы#

Схема в Postgres — это пространство имён внутри одной базы: в ней живут таблицы, перечисления, представления и функции. Иерархия выглядит так: кластер содержит базы, база содержит схемы, схема содержит таблицы. В каждой базе есть схема по умолчанию — public, и шестнадцать уроков трекер честно прожил в ней: CREATE TABLE без квалификатора создаёт таблицу именно там. Схема — не база: это самая частая путаница темы. Разделение доменов по схемам не разводит данные по разным серверам и даже по разным файлам на диске — оно разводит имена и правила внутри одной базы.

Зачем домену собственное пространство имён. Первое — границы видны в самом SQL: запрос к billing.spendings сам объявляет, что трогает бухгалтерию, и в ревью это видно без знания контекста. Второе — права: Postgres выдаёт доступы на уровне схемы, и можно завести роль, которой разрешено читать billing и запрещено менять public — segregated доступ без единой строчки кода приложения. Третье — имена: у схемы свой словарь, и таблица журнала может называться просто spendings, не споря с будущими сущностями задач. Четвёртое — жизненный цикл: домен можно выгрузить одной командой DROP SCHEMA, не выписывая по таблице.

Применяем к трекеру: журнал списаний из одиннадцатого урока переезжает в схему billing — выделенное пространство бухгалтерии. Важная деталь, о которой часто спотыкаются: внешний ключ через схемы законен. billing.spendings может ссылаться на todos из public — обе таблицы живут в одной базе, и ссылочная целостность не знает о наших доменных границах. Домены — договор людей; база продолжает видеть одно пространство данных.

Схема billing: шаг, которым домен получает имя

GO
-- db/migrations/000004_billing.sql: домен списаний получает собственную схему
CREATE SCHEMA billing;

CREATE TABLE billing.spendings (
    id BIGSERIAL PRIMARY KEY,                       -- int64 в Go
    todo_id BIGINT NOT NULL REFERENCES todos (id),  -- внешний ключ через схемы законен
    amount BIGINT NOT NULL,                         -- сумма в копейках
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()   -- время ставит база
);

-- db/queries/spendings/spendings.sql: имена квалифицированы схемой
-- name: CreateSpending :exec
INSERT INTO billing.spendings (todo_id, amount)
VALUES ($1, $2);

Два движения в одном шаге. Первое: CREATE SCHEMA billing — таблица создаётся сразу с префиксом, потому что «текущей схемы» у sqlc не существует: генератор не исполняет SET, и после CREATE SCHEMA ничего не переключается — в живой базе тоже лучше не полагаться на переключение, а писать полные имена. Второе: запрос уже квалифицирован — INSERT INTO billing.spendings; это и есть тот стиль, который переживёт разветвление конфига ниже. Миграция 000004 продолжает нумерацию канона: базовая схема, приоритет, мягкое удаление — и теперь домен бухгалтерии.

search_path: как sqlc видит неквалифицированные имена#

В живой базе за короткие имена отвечает search_path — список схем, в которых Postgres ищет неквалифицированные ссылки: SET search_path TO billing, public, и запрос FROM spendings найдёт таблицу в billing, если там она есть. Удобно, но именно на этом месте генератор и база расходятся в поведении, и урок обязан сказать это прямо: search_path sqlc не эмулирует. Каталог генератора собирается из schema-файлов, и неквалифицированное имя в запросе разрешается только в схеме по умолчанию — public. Настройки search_path в конфиге не существует: вопрос о ней в репозитории sqlc открыт как enhancement (Configure search_path), а оператор SET генератор не исполняет вовсе.

Практическое следствие одно, но железное: в проекте с несколькими схемами всё, что лежит вне public, пишется квалифицированно — и в DDL, и в запросах. Короткое FROM spendings при таблице billing.spendings не «не найдётся тихо» — генерация честно упадёт с ошибкой вида relation "spendings" (код 42P01, неопределённая таблица); падение здесь — лучший исход, потому что оно ловится на ноутбуке.

Есть у схем и подарочная сторона — имена в Go. Для таблиц вне схемы по умолчанию sqlc строит имя по шаблону схема_таблица: billing.spendings превращается в структуру BillingSpending, а перечисление billing.payment_status — в тип BillingPaymentStatus с константами вида BillingPaymentStatusCaptured. В public всё остаётся как прежде — канонический Todo без префиксов. Границы домена таким образом читаются прямо в коде приложения: один взгляд на сигнатуру, и видно, чью таблицу вы держите в руках.

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

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

-- db/migrations/000004_billing.sql: схема и домен
CREATE SCHEMA billing;

CREATE TYPE billing.payment_status AS ENUM ('captured', 'refunded');

CREATE TABLE billing.spendings (
    id BIGSERIAL PRIMARY KEY,
    todo_id BIGINT NOT NULL REFERENCES todos (id),
    amount BIGINT NOT NULL,
    status billing.payment_status NOT NULL DEFAULT 'captured'
);

-- db/queries/spendings/spendings.sql
-- name: CreateSpending :exec
INSERT INTO billing.spendings (todo_id, amount, status)
VALUES ($1, $2, $3);

Проверка: короткое имя против квалифицированного

Таблица создана как billing.spendings, в каталоге sqlc схема billing существует. Коллега пишет запрос SELECT * FROM spendings без квалификатора и запускает sqlc generate. Что произойдёт?

sqlc.yaml multiple: список sql, пакет на запись#

Теперь сторона конфига. Ключ sql в sqlc.yaml — не объект, а список: каждая запись — самостоятельный набор генерации со своим движком, своей схемой, своими запросами и своим генератором кода. Доки прямо показывают конфиг из двух записей с разными движками; у каждой записи набор ключей один: name — необязательный человекочитаемый идентификатор набора, engine, schema — папка миграций или файл, queries — папка запросов или список путей, gen — генератор, плюс database и rules для проверок. Ключ name при одном пакете бесполезен, при двух-трёх уже помогает читать конфиг: записи перестают быть безымянными блоками yaml.

Главное следствие списка: на каждую запись — свой Go-пакет. В gen.go пишутся package — имя пакета на языке Go, и out — каталог, куда ляжет сгенерированный код; разные out — разные каталоги — разные пакеты с независимыми импортами. Кто-то ищет это под словами sqlc.yaml multiple packages, и поисковик приводит ровно сюда: список sql — штатный, документированный способ держать в одном файле конфигурации сколько угодно наборов генерации.

И второе следствие, менее очевидное: записей можно сколько угодно, а команда одна. sqlc generate обходит весь список за один запуск — каждый пакет обновляется сам. Так прячется ещё и ответ на вопрос об организации sqlc: один файл конфигурации, несколько точек выхода, границы домена видны и в конфиге, и в импортах. Соседняя запись может отличаться даже движком: в доках это postgresql рядом с mysql; для трекера обе записи — postgresql, но дорога к восемнадцатому уроку про MySQL уже лежит в этой конструкции — конфиг к смене движка готов, останется выучить отличия синтаксиса.

Канон урока: один конфиг, два пакета

GO
# sqlc.yaml: список sql — по записи на домен
version: "2"
sql:
  - name: "todos"
    engine: "postgresql"
    schema: "db/migrations"
    queries: "db/queries/todos"
    gen:
      go:
        package: "tododb"
        out: "internal/store/tododb"
        sql_package: "pgx/v5"

  - name: "spendings"
    engine: "postgresql"
    schema: "db/migrations"
    queries: "db/queries/spendings"
    gen:
      go:
        package: "spendingdb"
        out: "internal/store/spendingdb"
        sql_package: "pgx/v5"

Разберём канон построчно. sql — список из двух записей, у каждой своё имя: todos и spendings. schema у обеих записей один — db/migrations: папка миграций остаётся единственным источником правды о схеме, как договорились в шестнадцатом уроке; обе записи собирают один и тот же каталог базы и потому видят одни и те же таблицы — включая чужой домен. Различаются записи queries (собственная папка домена) и gen.go: tododb едет в internal/store/tododb, spendingdb — в internal/store/spendingdb. Один sqlc generate обработает обе записи и обновит оба каталога. sql_package: pgx/v5 наследуется из канона второго урока — у обоих пакетов один драйвер, поэтому транзакционные сценарии через два домена остаются совместимыми.

Проверка: сколько запусков генерации

В sqlc.yaml две записи sql: tododb и spendingdb. Разработчик поправил запрос в db/queries/spendings и хочет обновить сгенерированный код. Сколько раз нужно запустить sqlc generate?

Разделение доменов: свои queries и свой пакет#

Конвенция курса «одна тема выборок — один файл» выросла в правило уровнем выше: один домен — папка. В db/queries/todos живут todos.sql — списки, фильтры и джойны задач — и users.sql — домен исполнителей, который задачами пользуется, но владеет своими таблицами; сюда же переезжают панель фильтров и страницы. В db/queries/spendings — spendings.sql, журнал списаний со своей схемой billing. Ключ queries у записи принимает папку — или список путей, если домену нужно собрать файлы из нескольких мест; ни один файл не принадлежит двум пакетам, и это не ограничение, а граница: запрос можно найти по домену, домен — по пакету.

Каждый пакет получает полный набор сгенерированных файлов: свой db.go с конструктором и интерфейсом соединения, свои модели, свой Querier — интерфейс, в котором только методы этого домена. Граница становится компиляторной: сервис задач принимает *tododb.Queries и физически не может вызвать CreateSpending — такого метода в его типе нет.

Где жить запросам на стыке? Правило владельца: запрос принадлежит домену главного данных. ListTodosWithUser — джойн задач с исполнителями — живёт в todos.sql: задачу показывает пользовательский интерфейс, пользователь здесь деталь задачи. Транзакция из одиннадцатого урока — закрыть задачу и записать списание — затрагивает оба домена, и с двумя пакетами сценарий не ломается: оба пакета построены над одним контрактом соединения, на одном транзакционном соединении можно держать по объекту Queries от каждого домена; подробности механики — в том уроке, здесь важна фактура: разветвление пакетов не ломает транзакции, потому что делит методы, а не соединения.

Дерево проекта и два импорта вместо одного

GO
# дерево после разветвления: домены получили папки и пакеты
db/
  migrations/            # одна папка схемы на все пакеты
    000001_init.up.sql
    000002_add_priority.up.sql
    000003_soft_delete.up.sql
    000004_billing.sql
  queries/
    todos/               # запись "todos" -> пакет tododb
      todos.sql          # задачи: списки, фильтры, джойны
      users.sql          # исполнители: свой домен в том же пакете
    spendings/           # запись "spendings" -> пакет spendingdb
      spendings.sql      # журнал: схема billing
internal/
  store/
    tododb/              # сгенерировано: db.go, models.go, querier.go
    spendingdb/          # сгенерировано: db.go, billing_models.go, querier.go

// internal/service/dashboard.go: два домена — два импорта
import (
        "tracker/internal/store/spendingdb"
        "tracker/internal/store/tododb"
)

// Dashboard держит по объекту Queries на домен: задачи в tododb,
// суммы — в spendingdb, где таблицы живут в схеме billing
type Dashboard struct {
        Todos     *tododb.Queries
        Spendings *spendingdb.Queries
}

Как читать дерево. Миграции не разветвились: сумма up-файлов по-прежнему описывает всю базу, и оба пакета смотрят на неё как на источник схемы. Запросы разветвились строго по доменам. Путь импорта складывается из модуля и out — tracker/internal/store/tododb, и в коде приложения теперь два импорта вместо одного. Имя billing_models.go в spendingdb — не опечатка: имя файла моделей настраивается, и на этом месте урока мы его настроим; по умолчанию файл называется models.go. Каждая из структур Queries компилируется независимо — правка домена задач не трогает журнал списаний.

Модели на стыке: кто получит какие структуры#

Раз оба пакета смотрят на одну папку миграций, честный вопрос — что окажется в моделях каждого. Ответ генератора прост до грубости: models.go каждого пакета строится по всему входу схемы. Раз в db/migrations описаны todos, users и billing.spendings, то и Todo, и User, и BillingSpending появятся в обоих пакетах — независимо от того, чьи запросы лежат в папке. Это не баг, а следствие устройства: генератор печатает модели по каталогу, а не по упоминаниям в запросах.

Дубль — не всегда зло: пакеты независимы, их можно версировать и переписывать порознь, и для среднего проекта две копии структур — приемлемая плата за простоту. Но у конструкции есть три ручки настройки, и урок перечисляет все три. Первая — omit_unused_structs: включённая, она оставляет в пакете только те структуры таблиц и перечислений, которые реально встречаются в его запросах; spendingdb перестаёт таскать Todo, если его запросы не касаются задач. Вторая — имена файлов: output_models_file_name меняет models.go на что угодно — например, billing_models.go; рядом живут output_db_file_name и output_querier_file_name для db.go и querier.go. Третья — само входное множество: schema может быть списком путей, и можно было бы развести миграции по доменам; но тогда внешний ключ billing.spendings → todos перестанет проверяться генератором в пакете spendings, потому что todos уйдёт из его поля зрения. Общая папка — компромисс, при котором границы проверяются целиком; усечение моделей дешевле усечения схемы.

Предскажите содержимое models.go

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

# sqlc.yaml — обе записи смотрят на одну схему:
#   schema: "db/migrations"   (там: todos, users, billing.spendings)
#   omit_unused_structs не включён — настройки по умолчанию

-- db/queries/spendings/spendings.sql — единственный файл записи "spendings"
-- name: CreateSpending :exec
INSERT INTO billing.spendings (todo_id, amount) VALUES ($1, $2);

-- name: TotalSpent :one
SELECT sum(amount)::bigint FROM billing.spendings WHERE todo_id = $1;

Усечение моделей и имя файла

GO
# sqlc.yaml: обе записи целиком — ручки настройки на месте
version: "2"
sql:
  - name: "todos"
    engine: "postgresql"
    schema: "db/migrations"
    queries: "db/queries/todos"
    gen:
      go:
        package: "tododb"
        out: "internal/store/tododb"
        sql_package: "pgx/v5"
        omit_unused_structs: false   # фасад задач: пусть несёт все модели

  - name: "spendings"
    engine: "postgresql"
    schema: "db/migrations"
    queries: "db/queries/spendings"
    gen:
      go:
        package: "spendingdb"
        out: "internal/store/spendingdb"
        sql_package: "pgx/v5"
        omit_unused_structs: true        # только то, что трогают запросы пакета
        output_models_file_name: "billing_models.go"

Канон с ручками. omit_unused_structs выключен в tododb — пакет задач остаётся общим фасадом, где модели всех таблиц под рукой; включён в spendingdb — бухгалтерский пакет держит только собственное. Усечение касается именно табличных моделей и перечислений: структуры параметров и результатов вроде CreateSpendingParams и TotalSpentRow рождаются из запросов и живут всегда. output_models_file_name задаёт имя файла моделей вместо models.go по умолчанию — в дереве выше spendingdb уже показан с billing_models.go; соседние ключи тем же способом переименовывают db.go и querier.go, если в одном каталоге генерируется несколько наборов. Один запуск sqlc generate обновит оба пакета с новыми настройками — ручки не требуют отдельной команды.

Общие типы: глобальные overrides#

Остаётся стык, который не решается ни схемами, ни пакетами: один и тот же смысл в двух доменах. Суммы в копейках хранятся BIGINT и в задачах (стоимость), и в журнале (списания); в Go хочется видеть их не голым int64, а типом Money из общего пакета types, с методами и гарантиями. Если настраивать переопределение в каждой записи отдельно, рано или поздно записи разъедутся: одна получит обновление, вторая — забудет.

Для этого у конфига есть верхний этаж — глобальные overrides. Блок overrides размещается на верхнем уровне sqlc.yaml, вне списка sql, и документируется фразой, которая почти дословно описывает наш случай: иногда одна и та же конфигурация нужна сразу нескольким наборам генерации. Внутри — два раздела: rename переименовывает поля структур по имени колонки во всех пакетах разом (классика — id в Identifier), а overrides задаёт отображение типов: db_type указывает тип базы, go_type — куда его спроецировать, вплоть до import, package и type в чужом модуле. У каждого правила глобального блока есть и ключ engine — он нужен только в конфигах, где определения различаются движками; в однодвижковом проекте его можно не писать, значение возьмётся из текущей записи.

Правило выбора этажа простое. Одинаковое для всех пакетов — наверх, в глобальный overrides: один источник, ноль расхождений. Различающееся — внутрь gen.go конкретной записи, где у каждого пакета свои локальные настройки. И аккуратность с go_type: тип из вашего модуля должен быть совместим со сканированием колонки — это уже ваш контракт с драйвером, генератор только вставляет имя.

Верхний этаж конфига: правила для всех пакетов разом

GO
# sqlc.yaml: глобальные overrides — до списка sql
version: "2"
overrides:
  go:
    rename:
      id: "Identifier"              # каждое поле id во всех пакетах станет Identifier
    overrides:
      - db_type: "pg_catalog.int8"  # BIGINT — суммы в копейках
        engine: "postgresql"
        go_type:
          import: "tracker/internal/types"
          package: "types"
          type: "Money"             # int64 заменяется на types.Money везде
sql:
  # ... две записи пакетов из канона выше — без изменений

Блок overrides стоит рядом с sql, на верхнем уровне, и действует на каждую запись списка. rename говорит: колонка id в любом пакете станет полем Identifier. overrides проецирует BIGINT (внутреннее имя pg_catalog.int8) на types.Money из вашего модуля — и tododb, и spendingdb ссылаются на один и тот же тип, никакого дубля смысла. Ключ engine в правиле — атрибут именно глобального этажа: он различает правила в многодвижковых конфигах, а в однодвижковом его можно опустить. После правки — sqlc generate, один запуск, и оба пакета переезжают на Money. Локальные переопределения внутри gen.go никуда не деваются: этажи складываются, локальное уточняет глобальное.

Проверка: какой этаж конфига

Команда решила: поле id во всех структурах всех пакетов должно называться Identifier. Где место этой настройки в sqlc.yaml с двумя записями?

emit-опции: короткое повторение перед разветвлением#

Настройки gen.go, которые второй и четвёртый уроки разбирали по одному, живут на пакет — и теперь это перестаёт быть деталью. Краткий словарь тем урока. emit_json_tags добавляет структурам json-теги: включайте там, где структуры едут в HTTP-ответы, а не во всём подряд. emit_interface печатает интерфейс Querier поверх структуры — граница для подмены реализации; пакет задач с панелью фильтров выигрывает от неё больше, чем журнал. emit_exact_table_names заставляет имена структур точно повторять имена таблиц без единения множественного числа. emit_empty_slices возвращает пустой срез вместо nil из выборок-списков. emit_pointers_for_null_types проецирует nullable-колонки в указатели — для pgx/v5 это текущий канон курса. Здесь важен не словарь, а принцип: значение каждой настройки по умолчанию одинаково во всех пакетах, но включать их можно по-разному — домены настраиваются порознь, и это осознанное решение, а не расхождение версий конфига.

Два пакета рядом: кто что получает#

АспектПакет tododbПакет spendingdb
Запись в sqlc.yamlname: todos, queries: db/queries/todosname: spendings, queries: db/queries/spendings
Вход схемыdb/migrations целикомdb/migrations целиком
Таблицы в SQLpublic: todos, usersbilling: spendings
package и outtododb → internal/store/tododbspendingdb → internal/store/spendingdb
МоделиTodo, User, BillingSpendingтолько затронутое запросами: BillingSpending (omit_unused_structs)
Файл моделейmodels.gobilling_models.go (output_models_file_name)

Восстановите шаг домена бухгалтерии

Три пропуска: создание схемы и два квалифицированных имени — в таблице и в запросе.

-- db/migrations/000004_billing.sql CREATE billing; -- пространство имён домена CREATE TABLE .spendings ( id BIGSERIAL PRIMARY KEY, todo_id BIGINT NOT NULL REFERENCES todos (id), amount BIGINT NOT NULL, -- сумма в копейках created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- db/queries/spendings/spendings.sql -- name: CreateSpending :exec INSERT INTO billing. (todo_id, amount) VALUES ($1, $2);

Соберите разветвление трекера на домены

Девять строк: проект с одним пакетом db превращается в два пакета — задачи и бухгалтерия. Шаги идут от папок к конфигу, от генерации к коду приложения.

Строки

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

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

Карточки: несколько схем и организация

Термин

Схема Postgres

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

Пространство имён внутри одной базы: таблицы, перечисления, представления, функции. Схема по умолчанию — public. Схема — не база: домены в схемах живут в одной базе и не теряют ссылочную целостность; внешний ключ через схемы законен.

1 / 6

Что дальше#

Трекер разветвился по всем этажам сразу. В базе журнал списаний получил собственную схему billing, и SQL-запросы читаются как объявления домена. В конфиге ключ sql стал списком из двух записей: schema у них общая — папка миграций остаётся единственной правдой о структуре, — queries делятся по доменам, а gen.go выдаёт два независимых пакета с собственными моделями и интерфейсами. Дубль моделей усекается omit_unused_structs, имена файлов настраиваются, а смыслы, общие для доменов, живут на верхнем этаже глобальных overrides. Так и собирается организация sqlc: граница домена — в схеме, в папке queries и в импорте пакета одновременно.

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

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

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

Вывод

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

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

  • Схема Postgres — пространство имён внутри одной базы; умолчание — public. Домен в собственной схеме даёт границы в SQL, права на уровне схемы и свободный словарь имён; внешний ключ через схемы законен.
  • sqlc разбирает CREATE SCHEMA, но search_path не эмулирует: неквалифицированные имена ищутся только в public, ключа настройки нет, SET не исполняется. Всё вне public — квалифицированные имена в DDL и запросах.
  • Таблицам и перечислениям вне public генератор добавляет префикс схемы: billing.spendings → BillingSpending, billing.payment_status → BillingPaymentStatus. В public — прежние простые имена.
  • sqlc.yaml multiple: ключ sql — список, каждая запись — name, engine, schema, queries и gen.go со своим package/out. Один sqlc generate обходит все записи; на домен — свой Go-пакет с собственными моделями и Querier.
  • Разделение доменов: один домен — папка queries и запись конфига; запрос на стыке принадлежит домену главного данных; транзакционные сценарии через два домена не ломаются — пакеты делят методы, а не соединения.
  • Модели строятся по всему входу схемы обоими пакетами: дубль усекается omit_unused_structs, имя файла меняет output_models_file_name; одинаковое для всех пакетов — в глобальные overrides (go.rename, go_type с import), различное — в локальные настройки gen.go.
  • emit-настройки живут на пакет и включаются порознь: emit_json_tags, emit_interface, emit_exact_table_names, emit_empty_slices, emit_pointers_for_null_types — словарь из второго и четвёртого уроков, применяемый на границах домена.

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

Разведите трекер на домены. Первое: примените к локальной базе шаг 000004_billing.sql — CREATE SCHEMA billing и таблицу billing.spendings со внешним ключом на todos; убедитесь в psql командой \dn, что схема появилась, и что INSERT в billing.spendings со ссылками на реальные задачи проходит. Второе: попробуйте создать таблицу без префикса сразу после CREATE SCHEMA и найдите, в какой схеме она оказалась; объясните письменно, почему sqlc не увидел бы её в billing. Третье: сломайте генерацию коротким именем — напишите SELECT * FROM spendings без квалификатора и прочитайте ошибку несуществующей таблицы; почините до FROM billing.spendings. Четвёртое: создайте папки db/queries/todos и db/queries/spendings, разнесите файлы по доменам и соберите канонический конфиг урока с двумя записями; запустите sqlc generate один раз и проверьте, что оба каталога internal/store обновились. Пятое: найдите в spendingdb структуру BillingSpending и объясните себе префикс по формуле схема_таблица; создайте в схеме billing перечисление payment_status и сверните его в BillingPaymentStatus. Шестое: включите omit_unused_structs в spendingdb, перегенерируйте и убедитесь, что Todo исчез из бухгалтерского пакета, а CreateSpendingParams остался; затем переименуйте файл моделей через output_models_file_name. Седьмое: вынесите общий тип Money в internal/types и подключите его глобальным overrides с db_type: pg_catalog.int8; проверьте, что поле Amount в обоих пакетах стало Money после одного запуска генерации. Восьмое: перепишите сводку Dashboard из урока на два импорта и два объекта Queries; опишите в README проекта, где живут границы доменов — в схеме, в папке queries или в импорте, и почему все три ответа правильные.

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

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

Почему структура называется BillingSpending, а не просто Spending?

Для таблиц вне схемы по умолчанию генератор строит имя по шаблону схема_таблица и приводит его к Go-стилю: billing_spendings → BillingSpending. Префикс сохраняет связь с квалифицированными именами SQL — в коде так же видно, чья таблица, как и в запросе. Настройки, отключающей префикс, нет: в схеме public префикс просто не нужен, там генерация остаётся прежней.

Как sqlc разрешает короткие имена таблиц, если схем несколько?

Никакой search_path-логики у генератора нет: неквалифицированное имя ищется только в схеме по умолчанию — public. Ключа search_path в конфиге не существует, а оператор SET он не исполняет. Если таблица живёт в другой схеме, ссылайтесь на неё квалифицированно — billing.spendings; короткое имя мимо public упадёт с ошибкой несуществующей таблицы на этапе генерации.

Стоит ли делить проект на пакеты, если таблиц пока мало?

Не обязательно: один пакет с файлами по темам — честный формат небольшого проекта. Пакеты стоит заводить, когда появился второй домен с собственными правилами жизни: свои права в базе, свой темп изменений, своя команда. Разветвление обратимо и дёшево: папки queries, записи в списке sql, один запуск генерации — а граница, которую охраняет компилятор, окупается с первого конфликта на ревью.

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

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

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

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

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

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

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

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

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