Сравнение ORM для PostgreSQL на Go: GORM против Ent против Bun против sqlc

Практический обзор ORM в Go с упором на код

Содержимое страницы

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

golang + postgresql

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

Другие полезные ссылки

Подписаться

Получайте новые материалы про системы, инфраструктуру и AI engineering.