Сравнение ORM для Go и PostgreSQL: GORM против Ent против Bun против sqlc
Практический, насыщенный кодом взгляд на ORM в Go
Содержимое страницы
Наиболее известные ORM для GO — это GORM, Ent, Bun и sqlc. Вот небольшое сравнение этих инструментов с примерами операций CRUD на чистом GO.
TL;DR
- GORM: богатый функционал и удобство; самый простой для быстрой доставки, но с большей накладной на выполнение.
- Ent (entgo.io): схема-код с сгенерированными, безопасными с точки зрения типов API; отлично подходит для крупных проектов и рефакторинга.
- Bun: легковесный, SQL-ориентированный конструктор запросов/ORM; быстрый с отличными функциями Postgres, явно по дизайну.
- sqlc (не ORM): пишите SQL, получайте безопасные с точки зрения типов функции Go; лучшая производительность и контроль, без магии на этапе выполнения.
Критерии выбора и быстрое сравнение
Мои критерии:
- Производительность: задержка/пропускная способность, избегаемая накладная, пакетные операции.
- DX: кривая обучения, безопасность типов, отлаживаемость, трение кодогенерации.
- Экосистема: документация, примеры, активность, интеграции (миграции, трассировка).
- Набор функций: отношения, жадная загрузка, миграции, хуки, эскейп-хаты для сырых SQL.
Инструмент | Парадигма | Безопасность типов | Отношения | Миграции | Эргономика сырых SQL | Типичный случай использования |
---|---|---|---|---|---|---|
GORM | ORM в стиле Active Record | Средняя (на этапе выполнения) | Да (теги, Preload/Joins) | Автомиграция (опционально) | db.Raw(...) |
Быстрая доставка, богатый функционал, традиционные CRUD-приложения |
Ent | Схема → кодогенерация → удобный API | Высокая (на этапе компиляции) | Первоклассные (edges) | Сгенерированные SQL (отдельный шаг) | entsql , пользовательский SQL |
Крупные кодовые базы, команды, занимающиеся рефакторингом, строгая типизация |
Bun | SQL-ориентированный конструктор запросов/ORM | Средняя–Высокая | Явные (Relation ) |
Отдельный пакет migrate | Естественный (конструктор + сырые) | Сервисы, ориентированные на производительность, функции 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: читаемые, предсказуемые запросы; небольшое, но активное сообщество; отличные особенности для PostgreSQL.
- sqlc: минимальные зависимости на этапе выполнения; хорошо интегрируется с инструментами миграций и CI; отлично подходит для команд, комфортно работающих с SQL.
Основные возможности
- Связи и загрузка с нетерпением: все обрабатывают связи; GORM (теги +
Preload
/Joins
), Ent (границы +.With...()
), Bun (Relation(...)
), sqlc (вы пишете соединения). - Миграции: GORM (автомиграция; осторожно в продакшене), Ent (сгенерированный/diff SQL), Bun (
bun/migrate
), sqlc (внешние инструменты). - Хуки/Расширяемость: GORM (колбэки/плагины), Ent (хуки/промежуточное ПО + шаблоны/генерация кода), Bun (промежуточное ПО для хуков запросов, простое сырое SQL), sqlc (композиция в вашем приложении).
- JSON/Массивы (Postgres): Bun и GORM имеют удобные помощники; Ent/sqlc обрабатывают через пользовательские типы или SQL.
Когда что выбирать
- Выберите GORM, если хотите максимальное удобство, богатые функции и быструю прототипизацию для традиционных CRUD-сервисов.
- Выберите Ent, если цените безопасность на этапе компиляции, явные схемы и долгосрочную поддерживаемость в крупных командах.
- Выберите Bun, если хотите производительность и явные запросы, похожие на SQL, с удобствами ORM, где это помогает.
- Выберите sqlc, если вы (и ваша команда) предпочитаете чистый SQL с безопасными по типу привязками Go и нулевой накладной на этапе выполнения.
Минимальный 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