Содержание урока · 13 разделов
- sqlc vet: недостающая ступень статического анализа sql
- Анатомия правила: CEL над метаданными запроса
- Карта CEL-переменных sqlc vet
- Границы строковых правил: contains и его ловушки
- EXPLAIN-правила: vet против плана
- Managed databases: база для проверки из конфига
- pganalyze и кольцо обратной связи
- линт sql в CI: культура правил
- Первая пятёрка правил, с которых стоит начать
- Границы vet и мост к следующему уроку
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
sqlc vet: недостающая ступень статического анализа sql#
К двадцать третьему уроку у трекера выстроена прочная вертикаль: схема в миграциях, запросы с аннотациями, сгенерированный пакет, проверка генерации в CI. И в этой вертикали есть слепая зона, которую стоит назвать вслух: всё, что мы проверяли, — это соответствие кода запросам. Статический анализ sql вводят поэтапно: сначала компиляция запросов против схемы, потом правила дисциплины, потом планы — sqlc vet кладёт все три ступени в одну команду. Ни одна машина до сих пор не отвечала на вопрос, хороши ли сами запросы. DELETE без WHERE, INSERT, который никто не должен вызывать, запрос с двадцатью параметрами, план, катящийся по таблице последовательным сканом, — компилятор Go пропускает это молча, потому что с точки зрения типов всё честно.
sqlc vet закрывает заново открывшуюся дыру. Команда появилась в версии 1.19.0 с формулировкой «runs queries through a set of lint rules» — прогоняет запросы через набор правил линта. Механика у неё двойная. Первая половина — без базы: vet собирает ваш конфиг, компилирует запросы по схеме, как это делает generate, а затем оценивает каждое правило-выражение против метаданных запроса. Вторая половина — с базой: если правило обращается к EXPLAIN-переменным, vet подключается к настроенному Postgres или MySQL, прогоняет каждый запрос через EXPLAIN и оценивает выражения уже по содержимому плана. Если выражение вычислилось в true — vet печатает сообщение правила и завершается с ненулевым кодом выхода. Это второе предложение — весь смысл команды в CI: ненулевой выход роняет шаг пайплайна, и плохой запрос доезжает до main только через осознанное решение человека, а не по инерции.
Слово «линт» здесь точнее слова «тест». Тест подаёт данные и смотрит на результат; линт смотрит на сам текст и метаданные и спрашивает: соответствует ли это нашей конвенции. sqlc vet — линт sql: правила живут в том же sqlc.yaml, что и весь остальной конфиг, ревьюятся в том же PR и действуют на каждый запрос пакета без единой строки тестового кода.
Канон урока: первые правила в sqlc.yaml
GOversion: "2"
sql:
- engine: "postgresql"
schema: "db/migrations"
queries: "db/queries"
gen:
go:
package: "db"
out: "db/sqlc"
sql_package: "pgx/v5"
rules: # какие правила ВКЛЮЧЕНЫ для этого пакета
- no-delete
- no-exec
- only-one-param
rules: # какие правила вообще ОПРЕДЕНЫ в проекте
- name: no-delete
message: "DELETE запрещён: мягкое удаление через deleted_at"
rule: |
query.sql.contains("DELETE")
- name: no-exec
message: ":exec у задач не нужен — возвращайте строку через :one"
rule: |
query.cmd == "exec"
- name: only-one-param
message: "больше одного параметра — повод вынести запрос на ревью"
rule: |
query.params.size() > 1Два уровня rules — главная путаница новичков. На верхнем уровне перечислены определения: имя, сообщение и выражение. Внутри записи sql — список имён, включённых для этого пакета: правило определено, но не активно, пока его не назвали в пакете. Выражения написаны на CEL — Common Expression Language, исполняются движком cel-go; срабатывает правило тогда, когда выражение вычислилось в true. Запуск — sqlc vet без аргументов: он прочитает конфиг, скомпилирует запросы и оценит каждое включённое правило против каждого запроса.
Проверка: когда правило срабатывает
Правило no-delete определено с выражением query.sql.contains("DELETE") и сообщением «DELETE запрещён». При каком условии sqlc vet сообщит об ошибке и выйдет с ненулевым кодом?
Анатомия правила: CEL над метаданными запроса#
CEL выбран не случайно: это маленький язык выражений от Google с детерминированной оценкой, коротким замыканием и безопасным песочничным исполнением — в нём нет циклов и обращений к внешнему миру, поэтому правило невозможно превратить в программу, которая что-то делает, кроме как считает. Каждому правилу vet подаёт два корня, описанные в документации proto-сообщениями. Первый — config: version и engine конфига, списки schema и queries; через него ловят, например, случайный движок в записи. Второй — query: sql — полный текст запроса, name — имя из аннотации, cmd — тип аннотации без двоеточия: one, many, exec, execrows, execlastid, batchexec и родня, params — список параметров, у каждого из которых есть number.
Из этих кирпичей собираются все «дисциплинарные» правила. config.engine == "mysql" — просыпаешься утром и видишь, что кто-то поменял движок в конфиге. query.cmd == "exec" — бан на аннотацию, которую команда договорилась не использовать. query.params.size() > 3 — красный флаг на запросы с кучей аргументов: не запрет, а фильтр «посмотри на это на ревью». query.name.contains("debug") — все экспериментальные запросы обязаны носить префикс, и линтер следит за конвенцией вместо вас.
Обратите внимание на природу данных: выражения видят метаданные запроса, а не его исполнение. vet не знает, что вернёт SELECT, сколько строк затронет UPDATE и какой индекс выберет база — для этого у него отдельный механизм, EXPLAIN-переменные, о которых ниже. Это деление стоит держать в голове при проектировании собственных правил: всё, что про текст и структуру, — в CEL без базы; всё, что про план и стоимость, — в CEL с подключённой базой.
Карта CEL-переменных sqlc vet#
| Выражение | Что даёт | Пример правила |
|---|---|---|
| config.engine | движок записи sql (postgresql, mysql, sqlite) | config.engine == "postgresql" |
| config.version | версия конфига — строка "2" | config.version == "2" |
| config.schema | пути к схеме (список) | config.schema.size() > 0 |
| query.sql | полный текст запроса | query.sql.contains("DELETE") |
| query.name | имя из аннотации -- name: | query.name.contains("debug") |
| query.cmd | аннотация без двоеточия: one, many, exec… | query.cmd == "exec" |
| query.params | список параметров запроса | query.params.size() > 3 |
| query.params[0].number | номер первого параметра | query.params[0].number == 1 |
| postgresql.explain | EXPLAIN-план (нужна база, v1.20.0+) | postgresql.explain.plan.total_cost > 100.0 |
| mysql.explain | EXPLAIN-план MySQL (нужна база) | has(mysql.explain.query_block.cost_info) |
Предскажите, какой запрос уронит vet
Прочитайте код и выберите, что он выведет:
-- db/queries/todos.sql — включены правила no-exec и only-one-param
-- name: ListDone :many
SELECT id, title FROM todos
WHERE done = true AND deleted_at IS NULL;
-- name: SoftDelete :execrows
UPDATE todos
SET deleted_at = now()
WHERE id = $1;
-- name: HardDelete :exec
DELETE FROM todos WHERE id = $1;Границы строковых правил: contains и его ловушки#
Первые правила, которые заводит команда, почти всегда строковые — и у них есть свойство, о котором документация честно умалчивает: contains смотрит на подстроку, а не на смысл. query.sql.contains("DELETE") сработает не только на DELETE FROM, но и на комментарии «-- DELETE: не трогать», на имени колонки deleted_at, на строке-литерале «DELETE» в данных запроса. Ложные срабатывания — не аргумент против линтера, а инструкция по его заточке.
Приёмы заточки три. Первый — сузить шаблон: contains("DELETE FROM") ловит уже не комментарий, а настоящую команду; продолжить можно регистром — contains("DELETE FROM") без учёта регистра в CEL даёт пара .lower() — query.sql.lower().contains("delete from"). Второй — направить правило в имя: конвенция «жёстких удалений нет» надёжнее держится на cmd — комбинация query.cmd.contains("exec") && query.sql.lower().contains("delete") точнее любого поиска по тексту. Третий — принять неточность как фичу: линтер, который подозрителен, полезнее линтера, который спит; ложное срабатывание стоит вам минуты, пропущенный DELETE без WHERE — вечера восстановления.
И общее правило гигиены: правила — это код команды, у них должен быть владелец. Правило, которое все отключают через @sqlc-vet-disable — об этой аннотации ниже, — не дисциплина, а ритуал; его либо переписывают, либо удаляют. Живой набор из трёх-пяти точных правил стоит дороже скопированного списка из двадцати чужих.
EXPLAIN-правила: vet против плана#
С версии 1.20.0 у vet появился второй уровень: выражения получили доступ к EXPLAIN-плану каждого запроса. Переменных две, и каждая привязана к своему движку: postgresql.explain и mysql.explain. sqlc заполняет только переменную вашего движка и только при настроенном подключении к базе — ключ database в записи sql с адресом uri. Для PostgreSQL vet выполняет EXPLAIN (ANALYZE false, VERBOSE, COSTS, SETTINGS, BUFFERS, FORMAT JSON) с текстом вашего запроса и разбирает JSON-ответ в структуру; для MySQL — EXPLAIN FORMAT=JSON. Планировщик оценивает запрос, ничего не исполняя: ANALYZE false означает, что база не прогоняет запрос по-настоящему, — vet безопасен даже для тяжёлых INSERT.
Структура плана в выражениях читается по узлам. Классика из доков: postgresql.explain.plan.node_type == 'Seq Scan' — корневой узел плана оказался последовательным сканом; postgresql.explain.plan.total_cost > 100.0 — суммарная оценка стоимости выше порога. Для MySQL зеркальные проверки живут глубже: mysql.explain.query_block.cost_info.query_cost и mysql.explain.query_block.table.key != 'PRIMARY' — «запрос обязан ходить по первичному ключу», причём перед обращением к необязательным полям выражение защищают функцией has() — has(mysql.explain.query_block.cost_info) && double(...) > 2.0. Структуры большие и зависят от формы запроса, поэтому доки честно советуют: сначала посмотрите сырой JSON через SQLCDEBUG=dumpexplain=1, потом пишите правило.
EXPLAIN-правила и подключение базы
GOversion: "2"
sql:
- engine: "postgresql"
schema: "db/migrations"
queries: "db/queries"
database:
uri: "postgres://postgres:postgres@localhost:5432/tracker?sslmode=disable"
gen:
go:
package: "db"
out: "db/sqlc"
sql_package: "pgx/v5"
rules:
- no-seq-scan
- not-too-costly
rules:
- name: no-seq-scan
message: "план уходит в Seq Scan — нужен индекс или запрос полегче"
rule: |
postgresql.explain.plan.node_type == 'Seq Scan'
- name: not-too-costly
message: "оценка стоимости планировщика слишком высока"
rule: |
postgresql.explain.plan.total_cost > 100.0Ключ database.uri в записи sql — то, что включает EXPLAIN-переменные; без него vet просто не имеет базы, откуда взять план, и такие правила не оценить. Два классических правила из доков: проверка корневого узла плана на Seq Scan и порог на total_cost — суммарную оценку стоимости планировщика. Отладка правил — через переменную окружения SQLCDEBUG=dumpexplain=1: sqlc напечатает сырой JSON плана по каждому запросу, и вы увидите, где в структуре лежит нужное поле; временная dummy-правило вида !has(postgresql.explain) заставит vet прогнать EXPLAIN по всем запросам только ради дампа.
Проверка: почему vet упал на чистой базе
Команда подключила database.uri на свежую базу стенда и запустила vet с правилом no-seq-scan. vet упал с ошибкой про отсутствие таблиц. Кто виноват и что делать?
Managed databases: база для проверки из конфига#
Требование «живая база с актуальной схемой» рождает очевидную рутину: где-то нужно поднять Postgres, накатить миграции, дать vet адрес. С версии 1.22.0 sqlc забрал эту работу на себя через managed databases, а с версии 1.27.0 — научился делать это локально: sqlc создаёт эфемерную базу, накатывает вашу схему, прогоняет через неё что нужно и убирает за собой. Конфиг прост: запись в топ-уровневом ключе servers — движок и uri доступного сервера, — а в записи sql вместо database.uri пишется database.managed: true. Наличие сервера по-прежнему нужно: эфемерная база создаётся на нём, поэтому локально — ваш Docker с Postgres, в CI — сервис-контейнер.
Что это меняет на практике. Во-первых, EXPLAIN-правила перестают требовать ручной оркестрации: managed база рождается уже с вашей схемой, и vet оценивает планы сразу. Во-вторых, выигрывает сам generate: с подключённой базой sqlc анализирует сложные запросы через настоящий планировщик и точнее выводит типы — признание из доков: без базы sqlc does its best… with managed databases configured, sqlc generate will automatically create a hosted ephemeral database with your schema. В-третьих, конфиг становится самодостаточным для CI: сервер один и тот же для тестов и для vet.
Оговорка про историю, чтобы вы не тратили вечер на старые туториалы: хостинговые эфемерные базы sqlc Cloud, с которых начиналась фича, выключены с сентября 2024 года — рабочая схема сегодня это managed: true плюс ваш сервер из servers, а старые конфиги с cloud.project мигрируют на неё.
sqlc/db-prepare поверх managed database
GOversion: "2"
servers:
- engine: postgresql
uri: "postgres://localhost:5432/postgres?sslmode=disable"
sql:
- engine: "postgresql"
schema: "db/migrations"
queries: "db/queries"
database:
managed: true # sqlc сам создаст эфемерную базу с вашей схемой
gen:
go:
package: "db"
out: "db/sqlc"
sql_package: "pgx/v5"
rules:
- sqlc/db-prepare # встроенное правило: prepare каждого запросаТри ключа складываются в конвейер: servers описывает доступный сервер Postgres, database.managed: true велит sqlc поднять на нём эфемерную базу с вашей схемой, правило sqlc/db-prepare прогоняет каждый запрос через prepare против этой базы. Это единственное встроенное правило — для всего остального пишете выражения сами. URI сервера можно отдать через окружение: uri: ${DATABASE_URI} — конфиг не тащит адреса стенда в git. Managed-режим работает и для generate, и для vet: одна настройка кормит обе команды.
Проверка: @sqlc-vet-disable
Один легаси-запрос закономерно нарушает новое правило, чинить его пока нельзя. Какой аннотацией вы выключаете vet для этого запроса?
Считайте @sqlc-vet-disable аптечкой: годится для одного запроса, опасна как режим жизни. Если у одного правила три и больше исключений в кодовой базе — правило не работает: либо оно неверно сформулировано (не учтён легитимный случай), либо команда с ним не согласна. Хороший тест: за каждый отключённый запрос в PR пишется одна строка почему. Не можете сформулировать — правило удаляйте честно, мёртвое исключение хуже живого правила.
pganalyze и кольцо обратной связи#
EXPLAIN-правила в CI — это первый круг обороны, и у него есть природа истины, которую важно понимать: план в CI считается на пустой или тестовой базе, где статистика таблиц — заглушка. Планировщик Postgres принимает решения по статистике, и на проде, где в todos миллион строк, тот же запрос может выбрать совсем другой план. Поэтому второй круг ставят на живые данные, и здесь на сцену выходят инструменты мониторинга: pganalyze — сервис наблюдения за Postgres, который собирает реальные планы и статистику с работающей базы и на их базе подсказывает индексы — его Index Advisor строит гипотетические индексы через расширение HypoPG и показывает, как изменится план.
Связка с sqlc vet — не официальный плагин, а кольцо обратной связи, которое собирается из штатных деталей. vet в CI ловит очевидное до merge: Seq Scan на большом справочнике, стоимость выше порога. pganalyze на проде подтверждает и уточняет: вот запрос, который в бою пошёл по Seq Scan, вот рекомендуемый индекс. Рекомендация доезжает в проект как обычная миграция с CREATE INDEX — и на следующем прогоне vet с правилом no-seq-scan вы видите, что план исправился. В экосистеме есть и офлайн-вариант того же приёма: открытое расширение index_advisor от Supabase даёт функцию, которой скармливаешь текст запроса — она возвращает рекомендованные CREATE INDEX по стоимости до и после; его можно гонять на той же CI-базе, что и vet.
Итоговая дисциплина звучит так: lint-правила держат стиль и структуру, EXPLAIN-правила держат план на тестовых данных, мониторинг подтверждает план на настоящей нагрузке, а всё найденное возвращается в схему через миграции. Каждый инструмент делает своё, и вместе они закрывают вопрос «почему тормозит», который один vet ответить не может.
линт sql в CI: культура правил#
Технически всё готово: правила определены, база управляется из конфига, аннотации отключения под контролем. Осталась организационная часть — как внедрить vet так, чтобы он не превратился в источник бесконечных красных пайплайнов. Практика, которая работает: правило заходит в проект только после того, как вся кодовая база ему уже удовлетворяет. Заводите правила волнами — первая чистка, потом включение в CI, потом следующая волна. Включать пять правил разом на репозиторий с двумя сотнями запросов — способ получить кнопку «перезапустить с --force» вместо линтера.
Порядок для sqlc-проекта, проверенный многими командами: первый шаг — sqlc/db-prepare против managed базы: он не про стиль, а про честность — каждый запрос обязан готовиться против настоящей схемы. Вторая волна — структурные правила: бан на :exec там, где команда договорилась возвращать строки, ограничение на число параметров, конвенция имён. Третья — EXPLAIN-правила, когда есть CI-база с близкой к продовой схемой и наполнением: Seq Scan-сторож и стоимостной порог. Каждая волна — один PR, одно обсуждение, один список исключений.
В пайплайне vet живёт рядом с sqlc diff из двадцать второго урока: diff гарантирует, что сгенерированный код соответствует запросам, vet — что запросы соответствуют договорённостям команды. Обе команды быстры, обе не требуют прод-доступа, и обе должны ронять PR одинаково безжалостно. А чтобы правила не протухали, у vet есть невидимый союзник — ревью: изменение правила едет в PR, как и любой код, и история git отвечает на вопрос, почему с 14 марта DELETE без WHERE считается ошибкой.
Первая пятёрка правил, с которых стоит начать#
- sqlc/db-prepare — каждый запрос готовится против живой схемы: ловит опечатки в колонках и несоответствие типов
- no-delete — DELETE FROM запрещён в пользу мягкого удаления через deleted_at (трекер живёт по этому закону с шестого урока)
- only-one-param / лимит параметров — запросы с пятью и больше аргументами обязаны показаться на ревью
- no-exec — в пакете разрешены только :one и :many: каждая запись возвращает строку через RETURNING (пятый урок)
- no-seq-scan — правило по EXPLAIN-плану: справочники и выборки по id не имеют права ходить последовательным сканом
- конвенция имён — префикс debug_ для экспериментальных запросов, чтобы лишние не жили в проде без спроса
Соберите правило и подключение базы
Заполните пропуски: ключ определения правил, поле выражения, переменная плана и ключ включения правила в пакете.
Порядок внедрения sqlc vet
Расставьте шаги внедрения vet в рабочем проекте.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Границы vet и мост к следующему уроку#
Честные границы, чтобы линтер не казался волшебной палкой. vet не исполняет запросы: EXPLAIN даёт оценку планировщика, а не реальность — ANALYZE-прогон с настоящими числами остаётся делом мониторинга. Правила на CEL видят метаданные и структуру плана, но не знают вашего домена: почему этот запрос обязан быть :exec — знает команда, поэтому правила — кодифицированные договорённости, а не истина в последней инстанции. И vet не заменяет тесты: он проверяет запросы против схемы и конвенций, но не против бизнес-логики.
В масштабе курса vet — последний технический инструмент перед итоговыми развилками. Мы двадцать два урока строили SQL-first-мир: конфиг, типы, транзакции, пагинация, миграции, CI — и вот контрольная проверка над всем этим хозяйством. Осталось ответить на главный стратегический вопрос: sqlc — правильный выбор вообще? Рядом живут GORM с его тегами и Preload, ent с графом и генерацией, sqlx с тонкой надстройкой над database/sql, squirrel с билдером запросов — и у каждого есть дни, когда он честно лучше. Двадцать четвёртый урок — sqlc vs gorm vs ent: сравнение по типобезопасности, динамике, миграциям и команде, чтобы выбор слоя данных был решением, а не привычкой.
Карточки: sqlc vet
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- sqlc vet — статический анализ sql из версии 1.19.0: правила с именем, сообщением и CEL-выражением; выражение сработало в true — vet печатает сообщение и падает с ненулевым кодом.
- CEL-выражения видят config (engine, version) и query (sql, name, cmd, params); правила двух уровней: определения на верхнем уровне, включение — списком в записи sql.
- EXPLAIN-правила с версии 1.20.0 читают план: для Postgres — EXPLAIN FORMAT JSON с VERBOSE и COSTS; нужны database.uri или managed-база с актуальной схемой — sqlc миграции не применяет.
- Managed databases (v1.22.0, локальные с v1.27.0): servers + database.managed: true — sqlc сам поднимает эфемерную базу с вашей схемой для vet и для точного анализа в generate.
- Встроенное правило sqlc/db-prepare готовит каждый запрос против базы; @sqlc-vet-disable отключает правила для одного запроса — аптечка, а не режим жизни.
- Дисциплина внедрения: правило заходит в проект, когда кодовая база ему уже удовлетворяет; в CI vet стоит рядом с sqlc diff; кольцо с прод-мониторингом вроде pganalyze возвращает найденное в схему через миграции.
Домашнее задание
Первое: включите для трекера правила no-delete и no-exec из канона, добавьте временный запрос с DELETE FROM и :exec — убедитесь, что sqlc vet падает с обоими сообщениями, потом уберите нарушителя. Второе: поднимите Postgres в Docker, опишите servers с uri и включите database.managed: true; прогоните sqlc generate и sqlc vet со встроенным sqlc/db-prepare — проверьте, что все запросы готовятся против эфемерной базы. Третье: добавьте правило no-seq-scan и проверьте его на выборке по колонке без индекса; добавьте индекс миграцией, убедитесь, что план сменился и vet стал зелёным — запишите оба плана в шпаргалку через SQLCDEBUG=dumpexplain=1. Четвёртое: напишите своё правило-конвенцию — например, все запросы на :many обязаны содержать LIMIT — и проверьте его против существующего пакета; вынесите исключение через @sqlc-vet-disable для одного легитимного запроса и напишите в PR строку почему. Пятое: добавьте шаг sqlc vet в CI после sqlc diff и сломайте его намеренно — скриншот красного шага в шпаргалку; почините и объясните, в какую волну внедрения это правило должно было попасть.
Личные заметкиПусто
Вопросы и ответы по уроку
sqlc vet падает с правилом по EXPLAIN, хотя запрос хороший. Почему?
Самая частая причина — статистика. EXPLAIN-правила оценивают план, а план зависит от статистики таблиц, и на пустой CI-базе она скудна: планировщик честно говорит Seq Scan там, где на проде с данными будет index scan. Настройте CI-базу ближе к проду — наполнение, ANALYZE, — поднимите порог стоимости, а окончательный вердикт отдавайте монитору на живой базе: vet ловит очевидное, pganalyze подтверждает на реальной нагрузке.
Чем @sqlc-vet-disable отличается от удаления правила?
Областью и следом. Удаление правила выключает его для всего проекта навсегда; аннотация — только для одного запроса, и это решение видно в дифе рядом с кодом запроса. Рабочая норма: исключение обязано иметь причину в описании PR. Если исключений набирается много, правило не согласовано с командой — его переписывают или удаляют, а не глушируют по одному запросу.
Можно ли обойтись без базы и использовать только CEL-правила?
Да, и это нормальная первая волна: правила над config и query — no-delete, лимиты параметров, конвенции имён и аннотаций — работают без подключения, потому что видят метаданные запроса. База нужна, когда правило смотрит на план: postgresql.explain и mysql.explain заполняются только при настроенном database.uri или managed-режиме. Начните без базы, добавьте EXPLAIN-правила, когда CI уже поднимает Postgres для тестов.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
ent: графовая ORM с кодогенерацией
Курс ent: схемы, поля, связи, запросы, хуки, privacy, миграции и GraphQL. 25 уроков с примерами и проектом микроблога.
- Проектировать схемы и связи в ent
- Генерировать код и жить с кодогенерацией
- Писать запросы builder-стилем с типами
- Строить хуки, интерсепторы и privacy-правила