ORM и Query Builders в Go: sqlx, squirrel, sqlc, GORM, ent
Зачем знать: На middle 1 ваша команда решает: писать сырой SQL через pgx, использовать sqlx ради удобства, генерировать код через sqlc, или брать GORM. Каждый инструмент — компромисс между скоростью, читаемостью и магией. От правильного выбора зависит производительность, читаемость диффов и шансы выстрелить N+1 в проде. На собесе спросят: чем sqlc лучше GORM, что такое N+1, когда squirrel оправдан, какие проблемы с auto-migrate. Без этих знаний middle backend в РФ не работает.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- Под капотом / Best practices
- Gotchas
- Производительность
- Вопросы на собеседовании
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»Спектр инструментов работы с БД в Go:
raw SQL → query builder → ORM[pgx] [squirrel] [GORM, ent, bun] [sqlx] [sqlc]Чем правее — больше абстракции, больше магии, больше overhead. Чем левее — больше контроля и предсказуемости.
| Инструмент | Категория | Магия | Type safety | Популярность 2026 |
|---|---|---|---|---|
| pgx (raw) | Драйвер | Нет | Через Scan | Высокая |
| sqlx | Расширение stdlib | Низкая | Через StructScan | Высокая |
| squirrel | Query builder | Низкая | Низкая | Средняя |
| sqlc | Code generation | Compile-time | Высокая | Очень высокая |
| GORM | Active Record ORM | Высокая | Низкая (reflect) | Высокая, но критика |
| ent | Schema-as-code ORM | Высокая | Высокая | Растёт |
| bun | Lightweight ORM | Средняя | Средняя | Средняя |
Главная мысль: Go-сообщество в 2026 предпочитает sqlc + pgx для большинства проектов, потому что:
- Type-safe (compile-time).
- Близко к SQL (видно, что выполняется).
- Без магии, легко дебажить.
- Производительность как у raw.
GORM остаётся в legacy и в стартапах, где нужен быстрый CRUD. Но в production-системах от него часто уходят.
2. Под капотом / Best practices
Заголовок раздела «2. Под капотом / Best practices»2.1 Raw database/sql + pgx
Заголовок раздела «2.1 Raw database/sql + pgx»Уже разобрано в 15-database-sql-pgx.md. Кратко:
var u Usererr := pool.QueryRow(ctx, `SELECT id, name FROM users WHERE id=$1`, 42). Scan(&u.ID, &u.Name)Плюсы: полный контроль, нулевой overhead. Минусы: много boilerplate’а, при изменении схемы — менять все Scan’ы вручную.
2.2 sqlx (jmoiron/sqlx)
Заголовок раздела «2.2 sqlx (jmoiron/sqlx)»Расширение database/sql: добавляет StructScan, Get, Select, named queries.
import "github.com/jmoiron/sqlx"
db, _ := sqlx.Open("pgx", dsn)
type User struct { ID int64 `db:"id"` Name string `db:"name"`}
// Один row → structvar u Usererr := db.GetContext(ctx, &u, `SELECT id, name FROM users WHERE id=$1`, 42)
// Много rows → slicevar users []Usererr := db.SelectContext(ctx, &users, `SELECT id, name FROM users WHERE age > $1`, 18)
// Named querytype Filter struct{ Min int `db:"min"` }rows, err := db.NamedQueryContext(ctx, `SELECT * FROM users WHERE age > :min`, Filter{Min: 18})StructScan vs Scan
Заголовок раздела «StructScan vs Scan»StructScan использует reflection, чтобы матчить колонки SELECT id, name с полями User{ID, Name} по тэгу db. Магии немного — только reflection при чтении.
Плюсы: меньше boilerplate, обычные SQL-запросы. Минусы: reflection (медленнее на ~10-20%, но обычно неважно), runtime ошибки при опечатках в SQL.
2.3 squirrel (Masterminds/squirrel)
Заголовок раздела «2.3 squirrel (Masterminds/squirrel)»Query builder с fluent API:
import sq "github.com/Masterminds/squirrel"
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
q := psql.Select("id", "name"). From("users"). Where(sq.Eq{"age": 18}). OrderBy("name"). Limit(10)
sql, args, err := q.ToSql()// sql: SELECT id, name FROM users WHERE age = $1 ORDER BY name LIMIT 10// args: [18]
rows, err := db.QueryContext(ctx, sql, args...)Композиция:
q := psql.Select("*").From("users")if filter.MinAge > 0 { q = q.Where(sq.GtOrEq{"age": filter.MinAge})}if filter.Name != "" { q = q.Where(sq.Like{"name": "%" + filter.Name + "%"})}Использовать: для динамических запросов (фильтры из UI). Альтернатива — ручная конкатенация SQL, что чревато ошибками.
Не использовать: для статичных запросов — sqlc или sqlx читаемее.
2.4 sqlc (kyleconroy/sqlc → sqlc-dev/sqlc)
Заголовок раздела «2.4 sqlc (kyleconroy/sqlc → sqlc-dev/sqlc)»Генерация Go-кода из SQL-файлов. Самый рекомендуемый подход в 2026.
Установка
Заголовок раздела «Установка»go install github.com/sqlc-dev/sqlc/cmd/sqlc@latestКонфиг sqlc.yaml
Заголовок раздела «Конфиг sqlc.yaml»version: "2"sql: - engine: "postgresql" queries: "internal/db/queries" schema: "internal/db/migrations" gen: go: package: "db" out: "internal/db" sql_package: "pgx/v5" # генерировать под pgx emit_interface: true # генерировать Querier интерфейс emit_json_tags: true emit_prepared_queries: true # авто-prepareQueries (queries/users.sql)
Заголовок раздела «Queries (queries/users.sql)»-- name: GetUser :oneSELECT id, name, age FROM users WHERE id = $1;
-- name: ListUsers :manySELECT id, name, age FROM users WHERE age > $1 ORDER BY name LIMIT $2;
-- name: CreateUser :oneINSERT INTO users (name, age) VALUES ($1, $2) RETURNING id;
-- name: UpdateUser :execUPDATE users SET name = $1 WHERE id = $2;
-- name: DeleteUser :execrowsDELETE FROM users WHERE id = $1;Аннотации:
:one— одна строка, возвращает struct.:many— slice struct’ов.:exec— без результата.:execrows— возвращает rowsAffected.:batchexec,:batchone,:batchmany— pgx batch.
Генерация
Заголовок раздела «Генерация»sqlc generateСоздаёт internal/db/queries.sql.go с функциями:
type User struct { ID int64 Name string Age int32}
func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) { row := q.db.QueryRow(ctx, getUser, id) var u User err := row.Scan(&u.ID, &u.Name, &u.Age) return u, err}Использование
Заголовок раздела «Использование»import "myapp/internal/db"
pool, _ := pgxpool.New(ctx, dsn)q := db.New(pool)
user, err := q.GetUser(ctx, 42)Migration-aware
Заголовок раздела «Migration-aware»sqlc читает schema/ файлы (DDL) и проверяет, что SELECT’ы валидны. Если в SQL опечатка — sqlc generate упадёт с ошибкой.
- Type-safe на compile time.
- Видишь SQL, который выполняется.
- Близко к 0% overhead vs raw pgx.
- Интегрируется с pgx, lib/pq, mysql.
- IDE автокомплит на сгенерированных функциях.
- Динамические queries сложны (фильтры — нужен squirrel рядом).
- Сложные joins требуют ручного prep.
- Каждое изменение SQL —
sqlc generate(можно черезgo:generate).
pgx adapter
Заголовок раздела «pgx adapter»С sql_package: "pgx/v5" sqlc генерирует код, работающий с pgxpool.Pool / pgx.Tx. Это даёт доступ к pgx-фичам: pgxpool, batch, типам.
2.5 GORM (go-gorm/gorm v2)
Заголовок раздела «2.5 GORM (go-gorm/gorm v2)»Полноценный ORM в стиле Active Record. Magic-heavy.
import "gorm.io/gorm"import "gorm.io/driver/postgres"
type User struct { ID uint `gorm:"primaryKey"` Name string Age int CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` // soft delete}
db, _ := gorm.Open(postgres.Open(dsn))db.AutoMigrate(&User{}) // ОПАСНО в prod, см. ниже
// Createdb.Create(&User{Name: "alice", Age: 30})
// Readvar u Userdb.First(&u, 42)db.Where("age > ?", 18).Find(&users)
// Updatedb.Model(&u).Update("name", "alice2")
// Deletedb.Delete(&u, 42)Associations
Заголовок раздела «Associations»type User struct { ID uint Orders []Order // HasMany Profile Profile // HasOne}type Order struct { ID uint UserID uint User User}
db.Preload("Orders").First(&u, 42) // загрузит user + его ordersfunc (u *User) BeforeSave(tx *gorm.DB) error { u.Name = strings.ToLower(u.Name) return nil}Запускается автоматически.
Auto-migrate
Заголовок раздела «Auto-migrate»db.AutoMigrate(&User{}, &Order{})ОПАСНО в production:
- Не удаляет колонки.
- Не делает миграции данных.
- Изменения схемы непредсказуемы.
- Нет version control.
Используйте для локальной разработки и тестов, в prod — golang-migrate/atlas.
Soft delete
Заголовок раздела «Soft delete»С полем DeletedAt gorm.DeletedAt GORM не удаляет, а ставит deleted_at = NOW(). Все Find’ы автоматически добавляют WHERE deleted_at IS NULL. Удобно, но магия — забываешь и удивляешься “почему данные есть, но не находятся”.
Performance issues
Заголовок раздела «Performance issues»- N+1:
db.Find(&users)потом цикл сdb.Model(&u).Association("Orders").Find(&orders)— N+1 запрос. ЛечитсяPreload. - Reflection на каждый запрос — overhead 10-30%.
- Hooks в цикле — вызываются на каждый объект.
- Lazy loading ассоциаций — легко словить N+1.
Бенчмарки показывают: GORM ~3-5x медленнее sqlc/pgx на CRUD.
Критика GORM в Go-community
Заголовок раздела «Критика GORM в Go-community»- Active Record паттерн плохо ложится на Go (нет inheritance, нужны workarounds).
- Errors не возвращаются из chain —
db.Where(...).First(&u), чтобы пойматьgorm.ErrRecordNotFound, нужно после.Error. - Magic SQL — не видно, что выполняется (нужен logger).
- Trust issues — обновления могут менять поведение.
Многие команды мигрируют с GORM на sqlc после первого инцидента в проде.
2.6 ent (entgo.io/ent)
Заголовок раздела «2.6 ent (entgo.io/ent)»Open source от Facebook. Schema-as-code в Go, генерирует typed API.
package schema
import "entgo.io/ent"
type User struct{ ent.Schema }
func (User) Fields() []ent.Field { return []ent.Field{ field.String("name").NotEmpty(), field.Int("age").Positive(), }}
func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("orders", Order.Type), }}go generate ./entГенерирует:
u, err := client.User.Create().SetName("alice").SetAge(30).Save(ctx)u2, err := client.User.Query().Where(user.Age(30)).Only(ctx)- Type-safe queries (compile-time).
- Graph-like API (edges, traversal).
- Schema valida tion.
- Hooks, privacy, GraphQL integration.
- Steep learning curve.
- Очень магический code generation.
- Большой объём сгенерированного кода.
- Не для простых CRUD-проектов.
В Faceboook ent используется массово (Tao, etc.). В Go-сообществе — нишевый, для сложных доменов с графами.
2.7 bun (uptrace/bun)
Заголовок раздела «2.7 bun (uptrace/bun)»Наследник go-pg. Lightweight ORM с близким к SQL API.
import "github.com/uptrace/bun"
type User struct { bun.BaseModel `bun:"table:users"` ID int64 `bun:",pk,autoincrement"` Name string}
db := bun.NewDB(pgxstdlib, pgdialect.New())
var u Usererr := db.NewSelect().Model(&u).Where("id = ?", 42).Scan(ctx)
err = db.NewInsert().Model(&User{Name: "alice"}).Exec(ctx)Менее популярен, чем GORM, но более идиоматичен (SQL-первый подход).
2.8 Что выбрать
Заголовок раздела «2.8 Что выбрать»| Сценарий | Рекомендация |
|---|---|
| Простой CRUD-сервис, мало queries | sqlx |
| Type safety + сложные queries | sqlc (default) |
| Динамические фильтры из UI | squirrel рядом с sqlc |
| Прототип, нет времени на SQL | GORM (но осторожно) |
| Сложная graph-схема (соц. сеть) | ent |
| Команда из Python/Ruby, хочет Active Record | GORM или bun |
| High-performance critical | pgx (raw) или sqlc |
Default в 2026: sqlc + pgx + squirrel для динамики + golang-migrate для миграций.
2.9 Performance benchmarks
Заголовок раздела «2.9 Performance benchmarks»Бенчмарки сообщества (relative):
| Tool | SELECT one | INSERT | SELECT 1000 |
|---|---|---|---|
| pgx (raw) | 1.0x | 1.0x | 1.0x |
| sqlc + pgx | 1.05x | 1.02x | 1.03x |
| sqlx | 1.1x | 1.05x | 1.15x |
| squirrel + pgx | 1.05x | 1.05x | 1.05x |
| bun | 1.4x | 1.3x | 1.5x |
| ent | 1.5x | 1.4x | 1.8x |
| GORM | 2.5x | 3.0x | 4.0x |
(Это grossly approximate, зависит от запроса.)
Вывод: sqlc/pgx — почти zero-overhead. GORM — 3-5x медленнее.
2.10 N+1 проблема
Заголовок раздела «2.10 N+1 проблема»Что:
users := db.Find(&users) // 1 запросfor _, u := range users { db.Where("user_id=?", u.ID).Find(&u.Orders) // N запросов}// Итого: 1 + N запросовРешения:
- JOIN:
SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id. - Preload (GORM):
db.Preload("Orders").Find(&users). - WHERE IN: загрузить users, потом
SELECT * FROM orders WHERE user_id = ANY($1). - DataLoader: batch’инг асинхронных запросов (для GraphQL).
В sqlc — пишете JOIN явно, проблемы нет:
-- name: ListUsersWithOrders :manySELECT u.*, o.* FROM users uLEFT JOIN orders o ON o.user_id = u.id;2.11 Connection pooling
Заголовок раздела «2.11 Connection pooling»ORM’ы используют *sql.DB или pgxpool.Pool под капотом. Pool настройка та же:
sqlDB, _ := db.DB()sqlDB.SetMaxOpenConns(25)(GORM пример.)
В sqlc — берёте свой pgxpool и передаёте в New(pool).
2.12 Repository pattern
Заголовок раздела «2.12 Repository pattern»Чтобы не привязываться к конкретному ORM, заверните в интерфейс:
type UserRepo interface { Get(ctx context.Context, id int64) (User, error) Create(ctx context.Context, u User) (int64, error)}
// pgRepo использует sqlc внутриtype pgRepo struct { q *db.Queries }func (r *pgRepo) Get(...) {...}В тестах — мок репозитория, не БД.
2.13 Logging queries
Заголовок раздела «2.13 Logging queries»Все ORM’ы умеют логировать. В sqlc + pgx — pgx tracelog. В GORM:
import "gorm.io/gorm/logger"
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Info),})Для prod: только slow queries и errors, не каждый запрос.
2.14 Transactions в ORM
Заголовок раздела «2.14 Transactions в ORM»- sqlc: передаёшь
pgx.Txвdb.New(tx). - GORM:
db.Transaction(func(tx *gorm.DB) error { ... }). - ent:
tx, _ := client.Tx(ctx)затемtx.User.Create()....
См. 17-transactions-migrations.md.
3. Gotchas
Заголовок раздела «3. Gotchas»3.1 GORM auto-migrate в проде
Заголовок раздела «3.1 GORM auto-migrate в проде»Не делайте этого. Auto-migrate:
- Не удаляет колонки.
- Не делает rename.
- Не делает backfill.
- Не делает миграцию данных.
В проде — миграции через golang-migrate/atlas.
3.2 GORM nil pointer
Заголовок раздела «3.2 GORM nil pointer»var u *Userdb.First(u, 42) // PANIC: nil pointerПередавайте &User{}, не nil.
3.3 GORM ErrRecordNotFound
Заголовок раздела «3.3 GORM ErrRecordNotFound»err := db.First(&u, 42).Errorif errors.Is(err, gorm.ErrRecordNotFound) { ... }В sqlc/pgx — pgx.ErrNoRows/sql.ErrNoRows.
3.4 GORM bool zero value
Заголовок раздела «3.4 GORM bool zero value»db.Where(&User{Active: false}).Find(...) // WHERE active=false НЕ выполнится!GORM игнорирует zero values в struct-фильтрах. Используйте map: db.Where(map[string]any{"active": false}).
3.5 squirrel и Dollar placeholder
Заголовок раздела «3.5 squirrel и Dollar placeholder»По умолчанию squirrel генерирует ? (MySQL). Для Postgres:
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)Иначе будут ? в SQL, и Postgres упадёт.
3.6 sqlx StructScan и embedded structs
Заголовок раздела «3.6 sqlx StructScan и embedded structs»type Audit struct { CreatedAt time.Time `db:"created_at"`}type User struct { Audit Name string}sqlx поддерживает embedded — выберет колонки рекурсивно. Но с конфликтами имён бороться нужно вручную.
3.7 sqlc и нестандартные SQL функции
Заголовок раздела «3.7 sqlc и нестандартные SQL функции»sqlc парсит SQL и матчит типы из schema. Если используете extension (PostGIS, pg_trgm) — типы могут не определиться. Решение: :exec без возврата, или явные CAST.
3.8 sqlc и dynamic SQL
Заголовок раздела «3.8 sqlc и dynamic SQL»-- name: ListUsers :manySELECT * FROM users WHERE age > $1 AND (name = $2 OR name LIKE $3);Невозможно сделать условный WHERE. Лечится:
- squirrel для динамики.
- SQL с условными
COALESCE:
WHERE ($1::int IS NULL OR age > $1)3.9 ent очень много генерируется
Заголовок раздела «3.9 ent очень много генерируется»После go generate ./ent папка ent/ весит мегабайты. Это нормально, но в IDE можно тормозить.
3.10 GORM и timestamps в UTC
Заголовок раздела «3.10 GORM и timestamps в UTC»GORM пишет CreatedAt/UpdatedAt в local time по умолчанию. В проде с разными TZ серверов будут проблемы. Принудительно ставьте UTC:
db.Config.NowFunc = func() time.Time { return time.Now().UTC() }3.11 sqlx Get vs QueryRow.Scan
Заголовок раздела «3.11 sqlx Get vs QueryRow.Scan»err := db.Get(&u, "SELECT id, name FROM users WHERE id=$1", 42)// если 0 строк: err = sql.ErrNoRowsТо же поведение, что и QueryRow.Scan — sql.ErrNoRows sentinel.
3.12 GORM scope, scopes и забытый context
Заголовок раздела «3.12 GORM scope, scopes и забытый context»В Gorm v2 контекст передаётся через db.WithContext(ctx). Забыли — запрос не отменится. Лечение: middleware на router, всегда db.WithContext(ctx).
3.13 sqlc и nullable
Заголовок раздела «3.13 sqlc и nullable»В Go типах:
NOT NULLколонка →string,int64,time.Time.NULLколонка →sql.NullString,pgtype.Text(в pgx).
Можно настроить overrides в sqlc.yaml:
overrides: - column: "users.middle_name" go_type: "*string"3.14 GORM cycles в Preload
Заголовок раздела «3.14 GORM cycles в Preload»db.Preload("Orders.User").Find(&users)// orders загрузит, потом для каждого order загрузит user — а это те же users.// Циклический preload — лучше JOIN.3.15 Batch insert в ORM
Заголовок раздела «3.15 Batch insert в ORM»- GORM:
db.CreateInBatches(&users, 100)— 100 за раз. - sqlc: используйте
:copyfromили:batchexec(pgx). - pgx raw:
pool.CopyFrom.
3.16 GORM soft-delete и UNIQUE
Заголовок раздела «3.16 GORM soft-delete и UNIQUE»CREATE UNIQUE INDEX uniq_users_email ON users(email);С soft-delete: запись помечена удалённой, но email занят. Новая запись с тем же email не вставится. Решение: UNIQUE INDEX ... WHERE deleted_at IS NULL.
3.17 sqlc и foreign keys
Заголовок раздела «3.17 sqlc и foreign keys»sqlc видит схему через DDL. Если migration сначала добавляет FK, потом sqlc generate, всё ок. Если порядок нарушен — generate упадёт.
3.18 Reflect overhead в ORM
Заголовок раздела «3.18 Reflect overhead в ORM»GORM использует reflect почти на каждое поле. На больших struct’ах (50+ полей) overhead растёт. Sqlc — нет reflect, всё захардкожено.
4. Производительность
Заголовок раздела «4. Производительность»4.1 SELECT one
Заголовок раздела «4.1 SELECT one»| Подход | Latency (median, локально) |
|---|---|
| pgx | ~150µs |
| sqlc | ~155µs |
| sqlx | ~165µs |
| GORM | ~400µs |
4.2 INSERT batch
Заголовок раздела «4.2 INSERT batch»Вставка 10к строк:
| Подход | Время |
|---|---|
| sqlc + pgx CopyFrom | ~80ms |
| sqlc + pgx Batch | ~250ms |
| sqlx ExecBatch | ~600ms |
| GORM CreateInBatches | ~1.5s |
| GORM Create в цикле | ~8s |
4.3 SELECT N rows + ассоциации
Заголовок раздела «4.3 SELECT N rows + ассоциации»Для 1000 users с orders:
- N+1 (наивный): ~1.5s (1000 запросов).
- JOIN: ~50ms.
- Preload (GORM): ~80ms (2 запроса: users, orders WHERE IN).
4.4 Память
Заголовок раздела «4.4 Память»GORM держит metadata о всех зарегистрированных моделях. На 50 моделей — десятки MB.
sqlc — только сгенерированные struct’ы, никакого runtime metadata.
4.5 Compilation time
Заголовок раздела «4.5 Compilation time»sqlc генерация на 100 запросов — несколько секунд. ent на большом schema — минуты. GORM/sqlx — нет генерации.
4.6 Cache prepared statements
Заголовок раздела «4.6 Cache prepared statements»pgx (под sqlc) — кэширует. GORM — тоже использует prepared (если не SimpleProtocol).
4.7 Connection pool
Заголовок раздела «4.7 Connection pool»Все используют одинаковый pool (database/sql или pgxpool). Настройка MaxOpen/MaxIdle важна одинаково.
4.8 GORM scopes inflate
Заголовок раздела «4.8 GORM scopes inflate»db.Where("a=?", 1).Where("b=?", 2).Where("c=?", 3).Find(&u)GORM строит chain in-memory. Десятки Where — overhead. Лучше один Where("a=? AND b=? AND c=?", 1,2,3).
4.9 sqlc и slice queries
Заголовок раздела «4.9 sqlc и slice queries»Для SELECT ... WHERE id = ANY($1) sqlc генерирует:
func (q *Queries) GetUsersByIDs(ctx context.Context, ids []int64) ([]User, error)Передавайте slice — pgx сериализует в array.
4.10 GORM и SELECT *
Заголовок раздела «4.10 GORM и SELECT *»GORM по умолчанию SELECT *. Для оптимизации — db.Select("id, name").Find(&u). На таблицах с 30 колонками — экономия трафика.
5. Вопросы на собеседовании
Заголовок раздела «5. Вопросы на собеседовании»1. Чем sqlx отличается от database/sql?
sqlx — расширение, добавляет Get/Select/StructScan, named queries. Тот же *sql.DB под капотом.
2. Что такое sqlc и почему он популярен?
Code generation из SQL: пишешь .sql, sqlc генерирует type-safe Go функции. Близко к raw pgx по скорости, ловит ошибки на compile time.
3. Чем sqlc лучше GORM? sqlc — без магии, видно SQL, compile-time safety, в 3-5x быстрее. GORM — Active Record, runtime reflect, скрытые N+1.
4. Что такое squirrel?
Query builder с fluent API: Select("*").From("users").Where(sq.Eq{...}). Полезен для динамических queries.
5. Что такое N+1 проблема? Один запрос за главным списком + N запросов за деталями (по одному на каждый элемент). Лечится JOIN, Preload, WHERE IN.
6. Что такое Preload в GORM?
Заранее загрузить ассоциированные данные одним дополнительным запросом (WHERE IN). Решает N+1, но не идеально.
7. Чем ent отличается от GORM? ent — schema-as-code, type-safe queries, edges (graph). GORM — Active Record, runtime reflection. ent сложнее, но безопаснее.
8. Когда использовать ORM, а когда raw SQL? ORM — когда CRUD простой и команда не любит SQL. Raw/sqlc — когда нужна производительность, контроль, type safety.
9. Почему AutoMigrate в проде — плохо? Не удаляет колонки, не делает rename, не делает backfill, не version-controlled. Используйте golang-migrate/atlas.
10. Что такое soft delete?
Запись помечается deleted_at = NOW() вместо удаления. Все SELECT’ы фильтруют WHERE deleted_at IS NULL. В GORM — встроенное.
11. Какие минусы soft delete?
- Unique constraints ломаются (email duplicate).
- Запросы сложнее.
- Размер таблицы растёт.
- GDPR — иногда обязан удалять физически.
12. Что такое hooks в GORM?
BeforeSave, AfterCreate etc. — функции, вызываемые автоматически. Удобно, но магия (поведение зависит от типа).
13. Как избежать N+1 в sqlc? Писать SQL с JOIN или WHERE IN явно. В sqlc видно, что выполняется, проблем меньше, чем в GORM.
14. Что такое sqlc.yaml? Конфигурационный файл sqlc: engine, paths, output package, overrides типов, sql_package (pgx/v5).
15. Что такое emit_interface в sqlc?
Генерирует Go interface Querier со всеми методами. Удобно для моков в тестах.
16. Чем squirrel отличается от sqlc? squirrel — runtime построение query (динамическое). sqlc — compile-time (статическое). Можно использовать вместе.
17. Что такое bun? Lightweight ORM, наследник go-pg. SQL-first подход, более идиоматичный, чем GORM.
18. Какой ORM используется в Booking, Avito, Wildberries (по слухам)? Чаще: sqlx или sqlc + pgx. GORM реже, обычно в стартапах или старых проектах.
19. Что такое Active Record в Go?
Паттерн ORM, где struct = строка БД, методы на struct’е (.Save(), .Delete()). В Go плохо ложится из-за отсутствия наследования.
20. Что лучше для type safety: sqlc или ent? Оба type-safe на compile time. sqlc проще, ent — больше фичей (edges, schema validation).
21. Может ли sqlc работать с MySQL? Да, поддерживает PostgreSQL, MySQL, SQLite.
22. Что такое overrides в sqlc?
Замена типа Go для конкретной колонки: users.id → MyUserID вместо int64.
23. Что такое prepared statements в ORM? Закэшированные на сервере (PG) SQL-запросы. ORM может авто-кэшировать (pgx, GORM с prepared=true).
24. Можно ли смешивать sqlc и raw pgx? Да. sqlc для статических queries, pgx для динамики/спецслучаев (LISTEN, COPY).
25. Какие минусы code generation подхода?
- Лишний шаг в build (
go generate). - Большой объём сгенерированного кода в git.
- При изменении схемы — re-generate. Но плюсы (type safety, скорость) обычно перевешивают.
6. Practice
Заголовок раздела «6. Practice»Задача 1: Migrate с GORM на sqlc
Заголовок раздела «Задача 1: Migrate с GORM на sqlc»Возьмите простой проект на GORM (User CRUD), перепишите на sqlc + pgx. Замерьте: latency, время компиляции, размер бинаря.
Задача 2: Динамический фильтр с squirrel
Заголовок раздела «Задача 2: Динамический фильтр с squirrel»Реализуйте ListUsers(ctx, filter) с опциональными name, min_age, max_age. Используйте squirrel для построения query.
Задача 3: Установка sqlc
Заголовок раздела «Задача 3: Установка sqlc»Установите sqlc, создайте sqlc.yaml, опишите 5 запросов в queries.sql, сгенерируйте код, напишите main, выполните.
Задача 4: N+1 в GORM
Заголовок раздела «Задача 4: N+1 в GORM»Создайте User has many Order. Сначала наивный код с N+1 (логируйте queries), потом с Preload. Сравните latency.
Задача 5: ent quick start
Заголовок раздела «Задача 5: ent quick start»Создайте User schema в ent, сгенерируйте код, выполните CRUD. Сравните UX с sqlc.
Задача 6: Repository интерфейс
Заголовок раздела «Задача 6: Repository интерфейс»Заверните sqlc-генерированные методы в свой UserRepo интерфейс. Замокируйте в unit-тестах через testify/mock.
Задача 7: Бенчмарк
Заголовок раздела «Задача 7: Бенчмарк»Напишите бенчмарки на одно и то же действие (SELECT user by id × 10000) для: pgx raw, sqlc, sqlx, GORM. Получите цифры.
Задача 8: Soft delete pitfalls
Заголовок раздела «Задача 8: Soft delete pitfalls»Реализуйте users с soft delete + unique email. Удалите пользователя, попробуйте создать с тем же email. Найдите проблему, исправьте через partial index.
7. Источники
Заголовок раздела «7. Источники»- sqlc: https://sqlc.dev/, GitHub: https://github.com/sqlc-dev/sqlc.
- sqlx: https://github.com/jmoiron/sqlx, docs http://jmoiron.github.io/sqlx/.
- squirrel: https://github.com/Masterminds/squirrel.
- GORM: https://gorm.io/, docs.
- ent: https://entgo.io/.
- bun: https://bun.uptrace.dev/.
- Why sqlc (статьи): https://kyleconroy.com/blog/sqlc.
- GORM критика в Go community: HackerNews threads, https://www.reddit.com/r/golang.
- Benchmarks: https://github.com/uptrace/go-orm-benchmarks.
- Книга “Learning Go” Bodner (2nd ed., 2024) — глава по БД.
- Talk “Hello, sqlc” Kyle Conroy на GopherCon.