---
title: "Сравнение с GORM, ent, squirrel"
description: "sqlc vs gorm vs ent vs sqlx: где ловится ошибка, динамика запросов, миграции, N+1 и рантайм-слой — критерии выбора orm для go вместо привычки."
course: "sqlc: типобезопасный код из SQL-запросов"
url: "https://golangmaster.ru/courses/sqlc/lessons/24-sravnenie/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Сравнение с GORM, ent, squirrel

*Курс «sqlc: типобезопасный код из SQL-запросов», урок 24 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/sqlc/lessons/24-sravnenie/*

Тема урока — sqlc vs gorm vs ent: перед итоговым проектом честно выведем sqlc на ринг и посмотрим, против кого он выходил все двадцать три урока. Соседей четверо, и каждый живёт в соседних курсах самоучителя: [GORM](/courses/gorm/) — классическая ORM с моделями-тегами, [ent](/courses/ent/) — графовая ORM с кодогенерацией, sqlx — тонкая надстройка над database/sql, squirrel — билдер SQL для динамики. orm для go сравнение — жанр, в котором обычно побеждает вера, поэтому вместо лозунгов здесь критерии: где ловится ошибка в имени колонки, кто отвечает за миграции, как живёт динамика фильтров, что происходит с N+1 и сколько рантайм-слоя стоит между вашим кодом и драйвером. Ответ «sqlc во всём лучше» — неправда, и урок покажет дни, когда GORM, ent или sqlx честно сильнее. Урок отвечает на пять вопросов. Первый: как устроена карта альтернатив и по какой оси инструменты различаются в принципе. Второй: sqlc vs gorm — разница между «SQL первичен» и «модель первична» на конкретной опечатке. Третий: sqlc vs ent — два генератора с разными источниками правды. Четвёртый: что делают sqlx и squirrel и почему их полезно держать в одной сумке с sqlc. Пятый: алгоритм выбора слоя данных для нового проекта — по команде, по домену, по срокам. План курса — на [странице курса sqlc](/courses/sqlc/); первый набросок этой темы — [урок-знакомство](/courses/sqlc/lessons/01-znakomstvo/).

### sqlc vs gorm vs ent: карта поля

Сравнение sqlc vs gorm vs ent, а заодно sqlx и squirrel, ведётся по одному главному вопросу: что является источником правды о данных. У GORM источник правды — структуры с тегами: вы описываете модели, а ORM сам собирает SQL в рантайме по правилам конвенций и тегов. У ent источник правды — схема как код: пакеты ent/schema описывают поля и связи, а генератор строит типобезопасный API запросов. У sqlc источник правды — сам SQL: вы пишете запросы, генератор выводит из них типы и методы. sqlx и squirrel ни на что не претендуют: первый — умный сканер строк из database/sql в структуры, второй — конструктор текста запроса в рантайме; правду они не генерируют, а лишь помогают её исполнять.

Из этого главного вопроса вытекают все остальные различия, и сравнение становится не списком фич, а следствием позиции. Кто первичен — тот и проверяется: у sqlc компилятор-генератор проверяет SQL против схемы до сборки Go-кода; у ent компилятор проверяет построенный API после генерации; у GORM всё, что не проверил компилятор Go, остаётся на рантайме. Кто первичен — тому и миграции: у GORM это AutoMigrate по структурам, у ent — отдельный движок миграций поверх схемы, у sqlc — снаружи, golang-migrate или goose, как в [шестнадцатом уроке](/courses/sqlc/lessons/16-migracii/).

Ограничение рамки, чтобы сравнение было честным: все четыре кандидата — про реляционные базы и database/sql-мир; драйверы pgx, modernc и родня за ними общие. Монго-драйверы, ORM поверх NoSQL и сырые драйверы в этот ринг не входят.

### Один запрос четырьмя инструментами

```go
// Задача: выбрать незавершённые задачи по приоритету

// 1) sqlc: запрос — файл db/queries/todos.sql, код сгенерирован
-- name: ListOpen :many
SELECT id, title, priority FROM todos
WHERE done = false AND deleted_at IS NULL
ORDER BY priority;

todos, err := q.ListOpen(ctx)

// 2) GORM: запрос строится в рантайме по модели
type Todo struct {
    ID       int64
    Title    string
    Priority int16
    Done     bool
}
var todos []Todo
err := db.Where("done = ? AND deleted_at IS NULL", false).
    Order("priority").Find(&todos).Error

// 3) ent: запрос строится сгенерированным builder'ом
todos, err := client.Todo.Query().
    Where(todo.Done(false), todo.DeletedAtIsNil()).
    Order(ent.Asc(todo.FieldPriority)).
    All(ctx)

// 4) sqlx: SQL ваш, скан в структуры — библиотеки
var todos []Todo
err := db.Select(&todos,
    "SELECT id, title, priority FROM todos WHERE done = $1", false)
```

Четыре стиля одной выборки. sqlc: SQL в файле, аннотация :many, сгенерированный метод — единственная строка в вашем коде. GORM: SQL рождается в рантайме из цепочки вызовов, модель описана тегами и конвенциями. ent: builder сгенерирован из схемы — типобезопасно, но на языке ent, а не SQL. sqlx: SQL ваш целиком, библиотека лишь сканирует колонки в поля. Все четыре вернут одинаковый срез — разница в том, кто и когда поймает ошибку, если колонка в схеме зовётся иначе.

### Проверка: кто поймает опечатку в колонке

**Вопрос:** В таблице колонка называется priority, а в коде по ошибке написано priorty. Кто из инструментов поймает это раньше всего?

- GORM — при Find(&todos) на этапе построения SQL
- sqlx — при сканировании результатов Select
- ✅ sqlc — на этапе sqlc generate, до сборки Go-кода
- Никто: опечатка в имени колонки — это ошибка рантайма базы

sqlc компилирует запрос против схемы во время генерации: несуществующая колонка ломает sqlc generate с ошибкой — до go build, до тестов, до прода. ent поймает её тоже рано, но иначе: builder с полем todo.Priorty просто не скомпилируется — такого поля генератор не создал. GORM узнает о беде от базы в рантайме: SQL с priorty вернёт ошибку undefined column. sqlx — хуже всех: несуществующая колонка в SQL упадёт в рантайме, а опечатка в теге db:"priorty" может пройти совсем молча — поле останется нулевым.

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

Сравнение sqlc vs gorm — это столкновение двух определений удобства. GORM удобен, когда модель уже есть: описал структуру с тегами — и CRUD появился сам, включая ассоциации, хуки и миграцию AutoMigrate, которая достроит таблицы по структурам. Цена — двойная потеря контроля. Первая: SQL строится в рантайме, и «что за запрос уйдёт в базу» — вопрос рефлексии, конвенций имён и тегов; разница между Save и Updates, мягкое удаление с deleted_at, полуавтоматические обновления updated_at — всё это правила, которые надо знать наизусть. Вторая: ошибки типов между моделью и таблицей ловит не компилятор, а база — а она отвечает только в рантайме, у конкретного пользователя.

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

Экономика ошибки — решающий аргумент этой пары. Опечатка в колонке у GORM живёт до первого исполнения пути кода: тест не покрыл — на прод. У sqlc она не переживает generate: CI роняет пайплайн за минуты. Но и тут честность: GORM имеет сырой выход — db.Raw и Exec — и тот же мир SQL доступен и в ORM-проекте, как показывал [десятый урок GORM](/courses/gorm/lessons/10-raw/); только проверять эти raw-запросы его генератор, в отличие от sqlc, никак не будет.

### Предскажите поведение GORM-кода с опечаткой

```go
type Todo struct {
    ID    int64
    Title string
    Done  bool
}

var open []Todo
err := db.Where("don = ?", false).Find(&open).Error
// в таблице todos колонка называется done; deleted_at у модели нет
```

**Что выведет программа?**

- Ошибка компиляции: поля don нет в модели Todo
- ✅ Ошибка на рантайме от базы: колонка don не существует
- Запрос пройдёт, Done останется false у всех — GORM прощает
- Паника при Find: несоответствие модели и схемы

Строка в Where — это обычный текст: компилятор Go про неё не знает, модель Todo не проверяется, поэтому сборка проходит. GORM подставит параметр, отправит в базу SELECT ... WHERE don = false, и база ответит ошибкой undefined column — она приедет в err в рантайме, на том окружении, где исполнился код. В том же сценарии sqlc упал бы на generate: запрос не скомпилировался против схемы. Паники GORM тут не устроит, а модели deleted_at он не потребует — колонка в WHERE просто не упомянута.

### sqlc vs ent: два генератора, две философии

sqlc vs ent — пара ближе, чем кажется: оба генерируют код, оба переносят проверки на этап до рантайма, оба живут с кодогенерацией в CI. Разница — в направлении генерации. ent растёт из модели: вы описываете сущности и связи — edges — в схеме как код, генератор строит и типы, и API запросов, и миграции под эту схему; SQL остаётся делом ent. sqlc растёт из запросов: схема задана SQL-миграциями, запросы написаны SQL-ом, генератор выводит только типы и методы; запрос остаётся делом автора.

Отсюда практические следствия. Сложные связи — forte ent: many2many, сквозные графы, жадная загрузка With* на три уровня — в ent это одна цепочка, а в sqlc каждый запрос с джойнами вы пишете сами, как в [двенадцатом уроке](/courses/sqlc/lessons/12-dzhoiny/). Произвольные выборки — forte sqlc: оконные функции, CTE, специфичные для движка конструкции ent либо не выражает builder'ом, либо выражает через Raw, теряя проверку; в sqlc любой SQL, который поймёт парсер движка, становится типизированным методом. [Девятый урок ent](/courses/ent/lessons/09-zaprosy/) показывает, как приятен его builder, когда запрос вписывается в граф; [пятнадцатый урок этого курса](/courses/sqlc/lessons/15-dinamicheskie/) — как больно без оконных функций, если бы sqlc их не умел.

И про команду: ent-схема — это новый язык проекта, который надо учить всем; sqlc-запросы — это SQL, который команда и так знает. Обратная сторона: ent документирует модель кодом — по пакетам ent/schema видна вся картина данных; в sqlc-проекте карту данных собирают из миграций.

### sqlx и squirrel: надстройки без амбиций

Двое из четверки не претендуют на роль слоя данных — и именно поэтому полезны в любом проекте. sqlx — это database/sql с приятностями: Select и Get сканируют строки в структуры по тегам db, NamedQuery даёт именованные параметры, Rebind чинит плейсхолдеры между движками, In собирает список под IN. SQL вы пишете сами — sqlx ничего не генерирует и схему не знает; его роль — убрать из вашего кода ручной Scan на тридцать колонок. Ловушка единственная, но знаменитая: тег db с опечаткой не совпадает ни с одной колонкой — и sqlx по умолчанию молча оставит поле нулевым; защита — sqlx.Strict() или дисциплина тестов, а компилятор здесь не поможет никогда: теги не проверяются.

squirrel — конструктор текста запроса: sq.Select().From().Where() собирает SQL и аргументы в рантайме, сохраняя параметризацию. Его жанр — та самая динамика, которую статический sqlc не умеет: произвольные комбинации фильтров, условные JOIN, сборка отчётных запросов по частям. Он не сканирует, не знает схему и не проверяет ничего — выход squirrel это строка и аргументы, которые дальше едут в Exec или в QueryContext.

Почему оба живут в одной сумке с sqlc. sqlx может заменить скучный слой сканирования там, где sqlc не генерирует запросы — например, в редких динамических отчётах. squirrel закрывает динамику вместо COALESCE-хаков, когда комбинаций фильтров слишком много. Оба инструмента дружат с тем же *sql.DB или pgx-пулом, что и сгенерированный пакет: вы выбираете инструмент по запросу, а не по проекту.

### orm для go сравнение: sqlc, GORM, ent, sqlx

| Критерий | sqlc | GORM | ent | sqlx |
| --- | --- | --- | --- | --- |
| Источник правды | SQL-файлы и миграции | структуры с тегами | схема как код (ent/schema) | нет — SQL ваш |
| Когда ловится опечатка в колонке | sqlc generate, до сборки | рантайм, ошибка от базы | компиляция после генерации | рантайм; тег db — может молча |
| Кто пишет SQL | вы — всегда | ORM, кроме Raw | ent, кроме Raw | вы — всегда |
| Динамические фильтры | COALESCE-паттерн, sqlc.slice | билдер в рантайме | предикаты And/Or/Not | вручную или с squirrel |
| Миграции | снаружи: golang-migrate, goose | AutoMigrate по структурам | встроенные, atlas для версионных | снаружи, как у sqlc |
| Жадная загрузка связей | джойн или второй запрос руками | Preload | With* на всю глубину | руками |
| Рантайм-слой над драйвером | нет — только database/sql или pgx | есть: рефлексия, хуки, конвенции | есть: сгенерированный, без рефлексии | нет, кроме скана |
| Проверка SQL против схемы | каждый generate | нет | нет (SQL строит ent) | нет |
| Кривая входа | нужен уверенный SQL | низкая: модели и конвенции | средняя: учить схему и builder | низкая при знании SQL |
| Лучший жанр | сервисы со сложными выборками | быстрый CRUD, прототипы | домены со сложными связями | тонкий слой поверх SQL |

### Проверка: сценарий и выбор

**Вопрос:** Стартап строит MVP каталога: CRUD на пять таблиц, связи один-ко-многим, схему ещё наверняка менять, SQL-навыки команды — базовые. Какой инструмент даст самый быстрый честный старт?

- sqlc: типобезопасность важнее всего на раннем этапе
- ✅ GORM: модели и AutoMigrate дают CRUD и эволюцию схемы почти без SQL
- ent: граф связей закроет один-ко-многим лучше всех
- sqlx: минимум магии при базовом SQL

Классический день GORM: модель меняется — AutoMigrate достраивает колонку, CRUD и Preload работают из коробки, SQL-навыки не блокируют скорость. Пока домен маленький и предсказуемый, эта экономия реальна. sqlc дал бы больше контроля, но каждое изменение схемы влечёт правку запросов — на этапе «схему ещё наверняка менять» это трение. ent силён на сложных связях, но учит свою схему-язык — в MVP это тоже время. Ключ урока: выбор начинается не от лучшего инструмента, а от фазы проекта и команды.

### Динамика запросов: главный аргумент против sqlc и ответ

Самый честный минус sqlc в любом orm для go сравнение — статичность. Запросы живут в файлах, генерация фиксирует их структуру, и «собрать WHERE из произвольного набора фильтров» в лоб не выходит: нельзя конкатенировать строки в query.sql и скормить генератору. GORM и ent решают это цепочками вызовов, squirrel — сборкой текста; sqlc предлагает три пути, и все три уже разобраны в пятнадцатом уроке.

Первый — COALESCE-паттерн: один статический запрос, в котором каждый фильтр вежлив к пустому параметру — WHERE ($1 = '' OR title ILIKE '%' || $1 || '%'). Дискретные комбинации фильтров на два-три поля закрываются им без единой строки динамики. Второй — sqlc.slice и ANY($1::int[]) для списков id: черные дыры «IN с переменным числом аргументов» перестают существовать. Третий — признать границу: отчётный конструктор с двадцатью комбинациями условий — работа для squirrel или сырого pgx, и это нормально: 5 процентов запросов проекта могут жить вне sqlc, при этом 95 процентов остаются проверяемыми генератором.

Сравните экономику с GORM-путём. Там динамика бесплатна в коде, но каждый такой запрос — невидимка: ни схема его не проверяет, ни ревью не видит текст SQL. Здесь динамика стоит одного декоративного OR-условия в SQL, но запрос остаётся документированным, проверенным и переиспользуемым. Ошибка «наш фильтр сломал план» ловится vet-правилом из двадцать третьего урока — в ORM-мире тот же баг найдёт только мониторинг.

### COALESCE-фильтр против динамического билдера

```go
-- sqlc-путь: один статический запрос на все комбинации фильтров
-- name: SearchTodos :many
SELECT id, title, priority, done
FROM todos
WHERE deleted_at IS NULL
  AND ($1::int IS NULL OR priority = $1::int)
  AND ($2::text = '' OR title ILIKE '%' || $2::text || '%')
  AND ($3::boolean IS NULL OR done = $3::boolean)
ORDER BY priority, id
LIMIT 50;

// GORM-путь: WHERE собирается по условию
q := db.Model(&Todo{}).Where("deleted_at IS NULL")
if filter.Priority != nil {
    q = q.Where("priority = ?", *filter.Priority)
}
if filter.Title != "" {
    q = q.Where("title ILIKE ?", "%"+filter.Title+"%")
}
err := q.Limit(50).Find(&rows).Error
```

Две стратегии одной задачи. sqlc: вся динамика переехала в SQL через NULL-параметры — генератор выведет Params с указателями и sql.Null-типами, план запроса один, vet его проверит. GORM: код гибче, но SQL не существует нигде в виде текста — его собирает рантайм, и регрессия «забыл deleted_at IS NULL» не поймается ничем, кроме теста. Выбор — между проверяемой статикой и гибкой сборкой; середина — squirrel для этого одного запроса и sqlc для остальных.

### Допишите фильтр, который делает sqlc динамичным

Заполните пропуски COALESCE-паттерна: проверка пустоты параметра, оператор сопоставления и обёртка паттерна LIKE.

```go
-- name: FindUsers :many
SELECT id, name FROM users
WHERE 1 = 1
  AND ($1::text = ___ OR name ILIKE ___ || $1::text || '%')
  AND (___($2::int) OR age = $2::int);
```

Подсказка: Пустая строка для текста; процент открывает начало; целочисленный параметр, который может быть NULL, объявляется функцией sqlc с приставкой n.

Правильные ответы в пропусках: `''`, `'%'`, `sqlc.narg`

### Миграции и N+1: что каждый инструмент делает за вас

Две обязанности слоя данных, в которых различия не менее остры, чем в запросах. Миграции: GORM несёт AutoMigrate — структуру изменил, база достроена при старте; соблазнительно для MVP и обсуждаемо для прода: документация GORM прямо не рекомендует AutoMigrate как единственный способ ведения версионной схемы, а история изменений в git — это история из дифов структур, а не читаемый SQL. ent подходит серьёзнее: собственная миграционная система с atlas, versioned migrations и ревью файлов. sqlc миграций не делает вообще — это решение шестнадцатого урока: golang-migrate или goose ведут SQL-файлы, а sqlc честно проверяет по ним каждый запрос. Формула ясная: у GORM миграции — конвенция, у ent — продукт, у sqlc — соседний инструмент, и в двух случаях из трёх вы всё равно упрётесь в SQL-миграции, как только понадобится перенос данных, а не только DDL.

N+1 — вторая обязанность. GORM умеет Preload: один вызов переложит «задачи с авторами» в два SQL-запроса; ent — With*, сгенерированные жадные загрузки на любую глубину. У sqlc нет ничего — потому что и не нужно: связь двух таблиц — это JOIN, который вы пишете в запросе, и план у него один, а не два; связь «список + детали» — второй запрос с ANY. Проблема N+1 в sqlc-мире не исчезает — она переезжает в ваши руки: слепую выборку в цикле вы устроите сами, и ни Preload за вас её не вытянет. Зато ни один из сгенерированных методов не спрячет от вас лишний запрос — текст каждого на экране.

### Проверка: мифы о sqlc

**Вопрос:** Какое из четырёх утверждений о сравнении инструментов — правда?

- sqlc — это ORM с SQL-синтаксисом: он тоже генерирует SQL из моделей
- sqlc не умеет в динамику вообще: фильтры пользователя — причина уйти на ORM
- sqlc и squirrel конфликтуют: выбрав sqlc, второй инструмент запретить
- ✅ sqlc — не ORM: он генерирует код из запросов и не имеет рантайм-слоя

sqlc не ORM и не строит SQL: источником правды служат ваши запросы, а на выходе — типы и методы над database/sql или pgx, без рефлексии и конвенций в рантайме. Динамика у него есть — COALESCE-паттерн, sqlc.slice, а в крайнем случае соседние инструменты; запретов на squirrel нет — оба работают с одним соединением. Первый вариант описывает направление, противоположное sqlc: там модель рождает SQL, здесь SQL рождает модель.

### Предскажите поведение sqlx с опечаткой в теге

```go
type User struct {
    ID    int64
    Email string `db:"mail"`   // в таблице колонка называется email
}

var u User
err := db.Get(&u, "SELECT id, email FROM users WHERE id = $1", 42)
```

**Что выведет программа?**

- Ошибка от базы: колонка mail не существует
- Ошибка сканирования: sqlx не нашёл поле для колонки email
- ✅ err == nil, но u.Email == "": тег не совпал, поле молча осталось нулевым
- Паника: несоответствие тега и колонки

SQL корректен — база вернёт id и email; ошибка от базы не приедет. sqlx сканирует колонки в поля по тегам db: колонка email ищет поле с тегом db:"email", а его нет — по умолчанию такая колонка просто игнорируется, скан завершается без ошибки, и u.Email остаётся пустой строкой. Это классическая тихая ловушка sqlx: лечится sqlx.Strict()-режимом, который ругается на незанятые колонки, тестом на конкретные значения или переходом на sqlc, где структура генерируется из запроса и совпасть не может в принципе.

### Производительность и рантайм-слой

Производительность слоя данных складывается из двух частей: во что превращается ваш код и во что превращается SQL-запрос. Со второй частью все четыре инструмента равны: SQL исполняет база, и разница между «ORM построил плохой JOIN» и «вы написали плохой JOIN» — в виновнике, а не в цене. Первая часть различается по-настоящему.

sqlc не имеет рантайм-слоя: сгенерированный метод — это строка запроса, пачка аргументов и вызов QueryContext с ручным сканом в структуру — как будто вы написали database/sql-код сами. ent генерирует код без рефлексии: builder собирает запрос вызовами методов, цена — в порядке простых операций; overhead заметен на построении, а не на исполнении. GORM — единственный из четверки с рефлексией в рантайме: модели читаются тегами и полями на каждом вызове, отсюда честные расходы на построение каждого запроса и знаменитые сюжеты про «ORM медленный», большинство которых на деле — про N+1 и неудачные Preload, а не про рефлексию. sqlx почти голый: скан структур — его единственная работа.

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

### DX и команда: цена входа и цена выхода

Developer experience — категория времени, и у каждого инструмента она двуликая: цена входа и цена жизни. GORM даёт самую низкую цену входа: модели, конвенции, готовый CRUD — разработчик без SQL пишет работающий слой в первый день. Цена жизни — накопленная: нюансы Save и Updates, сюрпризы Preload с кастомными условиями, поведение мягкого удаления, отладка «какой SQL ушёл в базу» — всё это платят коллеги через полгода, и платят чаще там, где SQL-культура слабее, потому что инструмент её не требует.

sqlc наоборот: вход дорог — нужен уверенный SQL, аннотации, понимание типов и NULL-семантики; первых два дня команды без SQL-опыта проходят больно. Цена жизни — низкая и стабильная: запросы читаются в ревью как текст, ошибки ловятся на generate, новые люди изучают домен, читая SQL. Хозяйственный бонус: SQL-навыки, которые команда прокачивает в sqlc-проекте, переносятся — это не внутренний язык инструмента, а профессия. ent занимает середину: учить надо схему и builder, зато дальше компилятор ведёт за руку, а по пакетам ent/schema новичок читает карту домена быстрее, чем по папке миграций.

И организационный фактор — найм и ревью. SQL знают и мидлы, и сеньоры; ent-схему придётся объяснять каждому новому человеку; GORM-конвенции неявно требуют прочтения документации. В ревью sqlc-PR показывает SQL и код рядом — обсуждение запроса предметно; ORM-PR показывает цепочки вызовов, и «что уйдёт в базу» остаётся догадкой до запуска. Для команды, где SQL — сила, sqlc превращает эту силу в архитектуру; для команды, где её нет, GORM честнее признает этот дефицит и не будет его наказывать.

### Алгоритм выбора слоя данных

Расставьте шаги выбора инструмента для нового проекта.

Правильный порядок строк:

1. `Оценить SQL-культуру команды: кто будет писать и ревьюить запросы`
2. `Оценить домен: CRUD поверх предсказуемой схемы или сложные выборки и связи`
3. `Оценить фазу: MVP с меняющейся схемой или сервис с устоявшимся доменом`
4. `Для сложных связей и графа — примерить ent: With*, предикаты, карта схемы как код`
5. `Для быстрого CRUD-старта — примерить GORM: модели, AutoMigrate, Preload`
6. `Для сложных выборок и контроля — примерить sqlc: SQL в ревью, generate, vet`
7. `Проверить миграционную историю: кто ведёт схему — AutoMigrate, atlas или golang-migrate`
8. `Проверить динамику: хватит ли COALESCE-паттерна или нужна сборка запросов`
9. `Решить про гибриды: sqlc для ядра плюс squirrel или Raw для редких отчётов`
10. `Зафиксировать решение в ADR: критерии, альтернативы и цена отказа`

### Комбинации в одном проекте и когда sqlc — не выбор

Инструменты слоя данных не являются моногамными: соединение одно, а слои могут быть разными. Рабочие гибриды встречаются постоянно. sqlc плюс squirrel: 95 процентов запросов — сгенерированные методы, редкий отчётный конструктор собирает SQL в рантайме и едет через то же соединение. sqlc плюс GORM: домен, рождённый прототипом, живёт в GORM, а тяжёлые выборки постепенно переезжают в sqlc-пакет — миграция слоя данных без остановки разработки. ent плюс Raw: граф закрывает 90 процентов, экзотические оконные функции идут через raw. Общая химия одна: у всех общий пул соединений и общий мир типов database/sql, сгенерированный пакет от соседей не зависит.

Но и честные дни, когда sqlc — неправильный выбор. Динамические отчётные системы, где пользователь собирает произвольные запросы из сотен полей, — там статические файлы не выживут, и его место займёт билдер. Проект без SQL-культуры на старте — sqlc потребует того, чего у команды нет, и не даст взамен скорости прототипа. Быстрый disposable-скрипт на две таблицы — генерация и аннотации дороже задачи. И команды, чей домен — глубоко вложенные связи с политиками доступа на графе, найдут ent роднее: там граф — не надстройка, а модель мышления.

Формула урока: sqlc выигрывает там, где SQL сложный, стабильный и дорогой в ошибках — сервисы, платёжные домены, аналитические выборки. GORM выигрывает там, где сложность в скорости разработки, а не в запросах. ent — где сложность в связях. sqlx — везде, где вы и так пишете SQL и не хотите платить за скан.

### Финальная рамка выбора и мост к проекту

Соберём двадцать четыре урока в одну рамку. Источник правды определяет всё: SQL-файлы у sqlc дают проверку на generate и SQL-грамотность как актив команды; структуры у GORM дают скорость входа и рантайм как место ошибок; схема как код у ent даёт граф и компилятор ценой нового языка; надстройки sqlx и squirrel дают честный ноль магии. Динамика, миграции, N+1 и производительность — следствия этой позиции, а не независимые фичи. Решение стоит принимать по команде и домену, фиксировать в ADR и пересматривать при смене фазы — гибриды легальны.

Для трекера задач из этого курса ответ уже принят: SQL здесь сложный, команда — вы, и вы двадцать три урока прокачивали SQL-first-мир. Осталось доказать делом. Двадцать пятый, итоговый урок — репозиторий задач целиком: миграции, queries, транзакции, HTTP-хендлеры и тесты в одном проекте, где каждый слой из этих уроков займёт своё место. sqlc проект соберётся из деталей, которые вы уже знаете, — урок лишь расставит их по полкам.

### Карточки: sqlc против альтернатив

- **Главный вопрос, по которому различаются sqlc, GORM и ent?** — Источник правды: у sqlc — SQL-файлы, у GORM — структуры с тегами, у ent — схема как код; из этого следуют проверки, миграции и стиль запросов
- **sqlc vs gorm: где ловится опечатка в имени колонки?** — sqlc — на sqlc generate, до сборки Go; GORM — в рантайме, ошибкой от базы; строка Where() для компилятора — просто текст
- **sqlc vs ent: в чём разница двух генераторов?** — ent генерирует из схемы: типы, builder API, миграции, SQL пишет ent; sqlc генерирует из запросов: только типы и методы, SQL пишет автор
- **Что делают sqlx и squirrel и как они дружат с sqlc?** — sqlx — скан строк в структуры по тегам db (опечатка в теге молча оставит нуль); squirrel — билдер SQL в рантайме для динамики; оба живут с sqlc через одно соединение
- **Как sqlc закрывает динамические фильтры?** — COALESCE/NULL-паттерн в статическом запросе, sqlc.slice и ANY для списков, а произвольную динамику — squirrel или Raw через то же соединение (пятнадцатый урок)
- **Кто ведёт миграции у каждого инструмента?** — sqlc — снаружи: golang-migrate/goose; GORM — AutoMigrate по структурам; ent — встроенные + atlas versioned; N+1: GORM Preload, ent With*, sqlc — JOIN и второй запрос руками

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

- Критерий сравнения один — источник правды: SQL-файлы (sqlc), структуры с тегами (GORM), схема как код (ent), отсутствие претензий у надстроек (sqlx, squirrel).
- sqlc vs gorm: опечатка в колонке у sqlc ломает generate до сборки, у GORM — рантайм от базы; sqlc не имеет рантайм-слоя, GORM платит рефлексией и конвенциями, raw-выход — db.Raw.
- sqlc vs ent: оба генерируют и оба переносят проверки до рантайма; ent силён на связях и графе (With*, миграции atlas), sqlc — на произвольных выборках и SQL как навыке команды.
- sqlx — скан без магии (тихая ловушка тега db), squirrel — билдер для динамики; оба легально живут в одном проекте со sqlc через общее соединение.
- Динамика у sqlc — COALESCE-паттерн, sqlc.slice и выход на squirrel; миграции — golang-migrate/goose; N+1 — ваша ответственность в JOIN-ах и вторых запросах.
- Выбор — по команде и домену, а не по микробенчмарку: сложные стабильные выборки — sqlc, быстрый CRUD — GORM, граф связей — ent, тонкий слой — sqlx; гибриды фиксируются в ADR.

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

Первое: напишите один и тот же запрос тремя способами — sqlc-аннотацией, GORM-цепочкой и sqlx-строкой — и сломайте его опечаткой в колонке: соберите таблицу, кто что сказал и когда; зафиксируйте вывод в шпаргалку. Второе: возьмите панель фильтров из пятнадцатого урока и оцените её честно: сколько комбинаций закрыл COALESCE-паттерн, где начался squirrel; напишите один отчётный запрос squirrel'ом через то же соединение, что и sqlc-пакет. Третье: посчитайте N+1 на пальцах: выборка задач с авторами через sqlc (JOIN) и через GORM (без Preload и с Preload) — распечатайте SQL каждого варианта и объясните разницу планов. Четвёртое: оформите ADR на полстраницы: ваш проект, ваша команда, ваш домен — что выберете и по каким трём критериям; перечислите условия, при которых решение пересматривается. Пятое: прочитайте десятый урок GORM про Raw и сравните с sqlc: где raw-запросы проверяются, а где нет; напишите абзац, что это значит для проекта с сотней raw-вызовов.

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

### sqlc быстрее GORM — стоит ли менять слой данных ради производительности?

Ради одного только рантайма — почти никогда: у sqlc нет слоя над драйвером, а у GORM есть рефлексия, но разница — микросекунды на фоне сети и плана запроса. Реальные различия скорости делают запросы, а не инструменты: N+1 и плохие планы дороже любой рефлексии. Переезд с ORM на sqlc оправдан, когда важнее другое: проверка запросов на generate, читаемые в ревью SQL и контроль плана — производительность приходит следствием, а не причиной.

### Можно ли использовать sqlc и GORM в одном проекте?

Да, и это рабочий паттерн для эволюции: соединение общее — *sql.DB или pgx-пул удовлетворяет и DBTX сгенерированного пакета, и GORM. Типичная схема: легаси-CRUD остаётся в GORM, сложные и горячие выборки переезжают в sqlc-пакет с проверкой на generate и vet. Следите за границами ответственности в ревью и за тем, чтобы у каждой таблицы был один владелец-инструмент — иначе schema_истина размажется между тегами моделей и миграциями.

### Что выбрать начинающему: sqlc или ORM?

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