Сравнение ORM для PostgreSQL на Go: GORM против Ent против Bun против sqlc
Практический обзор ORM в Go с упором на код
Наиболее заметными ORM для GO являются GORM, Ent, Bun и sqlc. Ниже приведено небольшое сравнение этих инструментов с примерами операций CRUD на чистом GO.

TL;DR
- GORM: богатый функционал и удобство; самый простой вариант для «быстрого запуска», но имеет большие накладные расходы во время выполнения.
- Ent: схема как код с генерацией типобезопасных API; отлично подходит для крупных кодовых баз и рефакторинга.
- Bun: легкий SQL-ориентированный конструктор запросов/ORM; быстрый, с отличной поддержкой функций Postgres, явный по дизайну.
- sqlc (технически не ORM, но все же): пишите SQL, получайте типобезопасный Go; наилучшая производительность и контроль, без магии во время выполнения.
Критерии выбора и краткое сравнение
Мои критерии:
- Производительность: задержка/пропускная способность, избегаемые накладные расходы, пакетные операции.
- UX разработчика (DX): кривая обучения, типобезопасность, отладка, сложность генерации кода.
- Экосистема: документация, примеры, активность, интеграции (миграции, трейсинг).
- Набор функций: отношения (relations), жадная загрузка (eager loading), миграции, хуки, способы перехода на сырой SQL.
| Инструмент | Парадигма | Типобезопасность | Отношения (Relations) | Миграции | Удобство сырого SQL | Типичный случай использования |
|---|---|---|---|---|---|---|
| GORM | ORM в стиле Active Record | Средняя (во время выполнения) | Да (теги, Preload/Joins) | Авто-миграция (опционально) | db.Raw(...) |
Быстрая разработка, богатый функционал, стандартные CRUD-приложения |
| Ent | Схема → генерация кода → флюентный API | Высокая (на этапе компиляции) | Первичные (ребра/edges) | Генерируемый SQL (отдельный шаг) | entsql, пользовательский SQL |
Крупные кодовые базы, команды, часто делающие рефакторинг, строгая типизация |
| Bun | SQL-ориентированный конструктор запросов/ORM | Средняя–Высокая | Явные (Relation) |
Отдельный пакет для миграций | Естественный (билдер + сырой) | Сервисы, ориентированные на производительность, функции Postgres |
| sqlc | SQL → генерация функций (не ORM) | Высокая (на этапе компиляции) | Через SQL-джойны | Внешний инструмент (например, golang-migrate) | Это сам SQL | Максимальный контроль и скорость; команды, ориентированные на DBA |
CRUD на примере
Настройка (PostgreSQL)
Используйте pgx или нативный драйвер PG для инструмента. Пример DSN:
export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'
Импорты (общие для всех ORM)
В начало каждого файла с примером кода go добавьте:
import (
"context"
"os"
)
Мы смоделируем простую таблицу users:
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
GORM
Инициализация
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type User struct {
ID int64 `gorm:"primaryKey"`
Name string
Email string `gorm:"uniqueIndex"`
}
func newGorm() (*gorm.DB, error) {
dsn := os.Getenv("DATABASE_URL")
return gorm.Open(postgres.Open(dsn), &gorm.Config{})
}
// Авто-миграция (опционально; будьте осторожны в продакшене)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }
CRUD
func gormCRUD(ctx context.Context, db *gorm.DB) error {
// Создание
u := User{Name: "Alice", Email: "alice@example.com"}
if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }
// Чтение
var got User
if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }
// Обновление
if err := db.WithContext(ctx).Model(&got).
Update("email", "alice+1@example.com").Error; err != nil { return err }
// Удаление
if err := db.WithContext(ctx).Delete(&User{}, got.ID).Error; err != nil { return err }
return nil
}
Примечания
- Отношения через теги структур +
Preload/Joins. - Помощник для транзакций:
db.Transaction(func(tx *gorm.DB) error { ... }).
Ent
Определение схемы (в ent/schema/user.go):
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int64("id").Unique().Immutable(),
field.String("name"),
field.String("email").Unique(),
}
}
Генерация кода
go run entgo.io/ent/cmd/ent generate ./ent/schema
Инициализация
import (
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
_ "github.com/jackc/pgx/v5/stdlib"
"your/module/ent"
)
func newEnt() (*ent.Client, error) {
dsn := os.Getenv("DATABASE_URL")
drv, err := sql.Open(dialect.Postgres, dsn)
if err != nil { return nil, err }
return ent.NewClient(ent.Driver(drv)), nil
}
CRUD
func entCRUD(ctx context.Context, client *ent.Client) error {
// Создание
u, err := client.User.Create().
SetName("Alice").
SetEmail("alice@example.com").
Save(ctx)
if err != nil { return err }
// Чтение
got, err := client.User.Get(ctx, u.ID)
if err != nil { return err }
// Обновление
if _, err := client.User.UpdateOneID(got.ID).
SetEmail("alice+1@example.com").
Save(ctx); err != nil { return err }
// Удаление
if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }
return nil
}
Примечания
- Строгая типизация от начала до конца; ребра (edges) для отношений.
- Генерируемые миграции или использование вашего инструмента миграций.
Bun
Инициализация
import (
"database/sql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
_ "github.com/jackc/pgx/v5/stdlib"
)
type User struct {
bun.BaseModel `bun:"table:users"`
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",notnull"`
Email string `bun:",unique,notnull"`
}
func newBun() (*bun.DB, error) {
dsn := os.Getenv("DATABASE_URL")
sqldb, err := sql.Open("pgx", dsn)
if err != nil { return nil, err }
return bun.NewDB(sqldb, pgdialect.New()), nil
}
CRUD
func bunCRUD(ctx context.Context, db *bun.DB) error {
// Создание
u := &User{Name: "Alice", Email: "alice@example.com"}
if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }
// Чтение
var got User
if err := db.NewSelect().Model(&got).
Where("id = ?", u.ID).
Scan(ctx); err != nil { return err }
// Обновление
if _, err := db.NewUpdate().Model(&got).
Set("email = ?", "alice+1@example.com").
WherePK().
Exec(ctx); err != nil { return err }
// Удаление
if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }
return nil
}
Примечания
- Явные джойны/жадная загрузка с помощью
.Relation("..."). - Отдельный пакет
bun/migrateдля миграций.
sqlc
sqlc технически не является ORM. Вы пишете SQL, а он генерирует типобезопасные методы Go.
sqlc.yaml
version: "2"
sql:
- engine: postgresql
queries: db/queries
schema: db/migrations
gen:
go:
package: db
out: internal/db
sql_package: "database/sql" # или "github.com/jackc/pgx/v5"
Запросы (db/queries/users.sql)
-- name: CreateUser :one
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING id, name, email;
-- name: GetUser :one
SELECT id, name, email FROM users WHERE id = $1;
-- name: UpdateUserEmail :one
UPDATE users SET email = $2 WHERE id = $1
RETURNING id, name, email;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
Генерация
sqlc generate
Использование
import (
"database/sql"
_ "github.com/jackc/pgx/v5/stdlib"
"your/module/internal/db"
)
func sqlcCRUD(ctx context.Context) error {
dsn := os.Getenv("DATABASE_URL")
sqldb, err := sql.Open("pgx", dsn)
if err != nil { return err }
q := db.New(sqldb)
// Создание
u, err := q.CreateUser(ctx, db.CreateUserParams{
Name: "Alice", Email: "alice@example.com",
})
if err != nil { return err }
// Чтение
got, err := q.GetUser(ctx, u.ID)
if err != nil { return err }
// Обновление
up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
ID: got.ID, Email: "alice+1@example.com",
})
if err != nil { return err }
// Удаление
if err := q.DeleteUser(ctx, up.ID); err != nil { return err }
return nil
}
Примечания
- Используйте собственные миграции (например,
golang-migrate). - Для динамических запросов: пишите несколько вариантов SQL или комбинируйте с небольшим билдером.
Примечания по производительности
- GORM: удобен, но добавляет накладные расходы на рефлексию и абстракции. Подходит для типичного CRUD; следите за запросами N+1 (предпочитайте
Joinsили избирательныйPreload). - Ent: генерируемый код избегает рефлексии; хорош для сложных схем. Часто быстрее ORM с большой магией во время выполнения.
- Bun: тонкая обертка над
database/sql; быстрый, явный, отличный для пакетных операций и больших наборов результатов. - sqlc: по сути, производительность сырого SQL с типобезопасностью на этапе компиляции.
Общие советы
- Используйте pgx как драйвер (v5) и context везде.
- Предпочитайте пакетную обработку (
COPY, многострочныйINSERT) для высокой пропускной способности. - Профилируйте SQL:
EXPLAIN ANALYZE, индексы, покрывающие индексы, избегайте лишних круговых обращений. - Повторно используйте подключения; настраивайте размер пула в зависимости от нагрузки.
Опыт разработчика и экосистема
- GORM: крупнейшее сообщество, много примеров/плагинов; более крутая кривая обучения для продвинутых паттернов.
- Ent: отличная документация; шаг генерации кода — основной сдвиг в ментальной модели; супер дружелюбен к рефакторингу.
- Bun: читаемые, предсказуемые запросы; меньшее, но активное сообщество; отличные «приятности» для Postgres.
- sqlc: минимальные зависимости во время выполнения; хорошо интегрируется с инструментами миграции и CI; превосходен для команд, уверенных в SQL.
Ключевые особенности
- Отношения и жадная загрузка: все обрабатывают отношения; GORM (теги +
Preload/Joins), Ent (ребра +.With...()), Bun (Relation(...)), sqlc (вы пишете джойны). - Миграции: GORM (авто-миграция; будьте осторожны в продакшене), Ent (генерируемый SQL/diff), Bun (
bun/migrate), sqlc (внешние инструменты). - Хуки/Расширяемость: GORM (callbacks/плагины), Ent (хуки/мидлварь + шаблоны/генерация кода), Bun (хуки запросов, похожие на мидлварь, легкий сырой SQL), sqlc (композиция в слое вашего приложения).
- JSON/Массивы (Postgres): Bun и GORM имеют удобные помощники; Ent/sqlc обрабатывают через пользовательские типы или SQL.
Когда что выбрать
- Выбирайте GORM, если хотите максимальное удобство, богатый функционал и быстрое прототипирование для стандартных CRUD-сервисов.
- Выбирайте Ent, если цените типобезопасность на этапе компиляции, явные схемы и долгосрочную поддерживаемость в крупных командах.
- Выбирайте Bun, если хотите производительность и явные SQL-подобные запросы с комфортом ORM там, где это помогает.
- Выбирайте sqlc, если вы (и ваша команда) предпочитаете чистый SQL с типобезопасными привязками Go и нулевыми накладными расходами во время выполнения. sqlc также отлично подходит для стороны модели чтения в архитектуре CQRS на Go, где запросы формируются под вызывающие стороны, а не под доменные сущности, а явный SQL дает вам полный контроль над проекцией.
Если вы все еще взвешиваете выбор этой ORM относительно стиля интеграции и границ сервисов, этот обзор архитектуры приложений поможет рассмотреть это решение в более широком контексте продакшена.
Минимальный docker-compose.yml для локального PostgreSQL
version: "3.8"
services:
db:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
ports: ["5432:5432"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d app"]
interval: 5s
timeout: 3s
retries: 5
Пакеты и библиотеки ORM для GO
Другие полезные ссылки
- Шпаргалка Golang
- Производительность AWS lambda: JavaScript vs Python vs Golang
- Исправление ошибки Golang GORM AutoMigrate postgresql
- Переранжирование текстовых документов с помощью Ollama и модели эмбеддинга Qwen3 - на Go
- Переранжирование текстовых документов с помощью Ollama и модели ранжирования Qwen3 - на Go