Porównanie ORM dla PostgreSQL w języku Go: GORM vs Ent vs Bun vs sqlc

Praktyczny, kodowy przegląd ORM w GO ```

Page content

Najbardziej znane ORMy dla GO to GORM, Ent, Bun i sqlc. Oto mała porównanie ich z przykładami operacji CRUD w czystym GO.

golang + postgresql

TL;DR

  • GORM: bogaty w funkcje i wygodny; najłatwiejszy do „wysłania”, ale ma większy narzut czasu wykonywania.
  • Ent: schemat jako kod z wygenerowanymi, typowo bezpiecznymi API; doskonały do dużych baz kodu i refaktoryzacji.
  • Bun: lekki, budowniczy zapytań/ORM oparty na SQL; szybki z dobrymi funkcjami Postgres, jasny przez projekt.
  • sqlc (nie jest ORM, ale nadal): pisz SQL, otrzymuj typowo bezpieczny Go; najlepsza surowa wydajność i kontrola, brak magii czasu wykonywania.

Kryteria wyboru i szybkie porównanie

Moje kryteria to:

  • Wydajność: opóźnienie/przepustowość, unikalny narzut, operacje wsadowe.
  • DX: krzywa uczenia, bezpieczeństwo typów, debugowalność, tarcia związane z generowaniem kodu.
  • Ecosystem: dokumentacja, przykłady, aktywność, integracje (migracje, śledzenie).
  • Zestaw funkcji: relacje, ładowanie wsteczne, migracje, hooki, ujawnione wyloty SQL.
Narzędzie Paradygmat Bezpieczeństwo typów Relacje Migracje Ergonomia SQL surowego Typowy przypadek użycia
GORM ORM stylu Active Record Średnie (czasu wykonywania) Tak (tagi, Preload/Joins) Auto-migracja (opcjonalna) db.Raw(...) Szybkie dostarczanie, bogate funkcje, konwencjonalne aplikacje CRUD
Ent Schemat → generowanie kodu → płynny API Wysokie (czasu kompilacji) Pierwszoplanowe (krawędzie) Wygenerowany SQL (oddzielny krok) entsql, niestandardowy SQL Duże bazy kodu, zespoły intensywnie refaktoryzujące, ścisłe typowanie
Bun Budowniczy zapytań/ORM oparty na SQL Średnie–Wysokie Jawne (Relation) Oddzielny pakiet migracji Naturalny (budowniczy + surowy) Usługi świadczące z uwzględnieniem wydajności, funkcje Postgres
sqlc SQL → generowane funkcje (nie jest ORM) Wysokie (czasu kompilacji) Poprzez łączenia SQL Narzędzie zewnętrzne (np. golang-migrate) To jest SQL Maksymalna kontrola i szybkość; zespoły przyjazne dla administratorów baz danych

Przykład operacji CRUD

Konfiguracja (PostgreSQL)

Użyj pgx lub nativego sterownika PG narzędzia. Przykład DSN:

export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'

Importy (wspólne dla wszystkich ORM)

Na początku każdego pliku z go code przykład dodaj:

import (
  "context"
  "os"
)

Zamodelujemy prostą tabelę users:

CREATE TABLE IF NOT EXISTS users (
  id    BIGSERIAL PRIMARY KEY,
  name  TEXT NOT NULL,
  email TEXT NOT NULL UNIQUE
);

GORM

Inicjalizacja

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{})
}

// Auto-migracja (opcjonalna; ostrożnie w produkcji)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }

CRUD

func gormCRUD(ctx context.Context, db *gorm.DB) error {
  // Tworzenie
  u := User{Name: "Alice", Email: "alice@example.com"}
  if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }

  // Odczyt
  var got User
  if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }

  // Aktualizacja
  if err := db.WithContext(ctx).Model(&got).
    Update("email", "alice+1@example.com").Error; err != nil { return err }

  // Usunięcie
  if err := db.WithContext(ctx).Delete(&User{}, got.ID).Error; err != nil { return err }

  return nil
}

Uwagi

  • Relacje przez tagi struktury + Preload/Joins.
  • Pomocnik transakcji: db.Transaction(func(tx *gorm.DB) error { ... }).

Ent

Definicja schematu (w 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(),
  }
}

Generowanie kodu

go run entgo.io/ent/cmd/ent generate ./ent/schema

Inicjalizacja

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 {
  // Tworzenie
  u, err := client.User.Create().
    SetName("Alice").
    SetEmail("alice@example.com").
    Save(ctx)
  if err != nil { return err }

  // Odczyt
  got, err := client.User.Get(ctx, u.ID)
  if err != nil { return err }

  // Aktualizacja
  if _, err := client.User.UpdateOneID(got.ID).
    SetEmail("alice+1@example.com").
    Save(ctx); err != nil { return err }

  // Usunięcie
  if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }

  return nil
}

Uwagi

  • Silne typowanie end-to-end; krawędzie dla relacji.
  • Wygenerowane migracje lub użyj swojego narzędzia migracji.

Bun

Inicjalizacja

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 {
  // Tworzenie
  u := &User{Name: "Alice", Email: "alice@example.com"}
  if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }

  // Odczyt
  var got User
  if err := db.NewSelect().Model(&got).
    Where("id = ?", u.ID).
    Scan(ctx); err != nil { return err }

  // Aktualizacja
  if _, err := db.NewUpdate().Model(&got).
    Set("email = ?", "alice+1@example.com").
    WherePK().
    Exec(ctx); err != nil { return err }

  // Usunięcie
  if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }

  return nil
}

Uwagi

  • Jawne łączenia/ładowanie wsteczne z .Relation("...").
  • Oddzielny pakiet bun/migrate dla migracji.

sqlc

sqlc technicznie nie jest ORM. Pisz SQL; generuje typowo bezpieczne metody 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" # lub "github.com/jackc/pgx/v5"

Zapytania (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;

Generowanie

sqlc generate

Użycie

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)

  // Tworzenie
  u, err := q.CreateUser(ctx, db.CreateUserParams{
    Name: "Alice", Email: "alice@example.com",
  })
  if err != nil { return err }

  // Odczyt
  got, err := q.GetUser(ctx, u.ID)
  if err != nil { return err }

  // Aktualizacja
  up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
    ID: got.ID, Email: "alice+1@example.com",
  })
  if err != nil { return err }

  // Usunięcie
  if err := q.DeleteUser(ctx, up.ID); err != nil { return err }

  return nil
}

Uwagi

  • Przygotuj własne migracje (np. golang-migrate).
  • Dla dynamicznych zapytań: napisz wiele wariantów SQL lub połącz z małym budownikiem.

Uwagi dotyczące wydajności

  • GORM: wygodny, ale dodaje narzut refleksji/abstrakcji. Dobre dla typowych CRUD; uważaj na zapytania N+1 (preferuj Joins lub selektywne Preload).
  • Ent: generowany kod unika refleksji; dobre dla złożonych schematów. Często szybsze niż ciężkie ORMs z magią czasu wykonywania.
  • Bun: cienki nad database/sql; szybki, jawny, świetny do operacji wsadowych i dużych zestawów wyników.
  • sqlc: praktycznie wydajność surowego SQL z bezpieczeństwem czasu kompilacji.

Ogólne wskazówki

  • Używaj pgx jako sterownika (w5) i context wszędzie.
  • Preferuj operacje wsadowe (COPY, wielowierszowe INSERT) dla wysokiej przepustowości.
  • Profiluj SQL: EXPLAIN ANALYZE, indeksy, indeksy pokrywające, unikaj zbędnych podróży.
  • Odnawiaj połączenia; dostosuj rozmiar puli na podstawie obciążenia.

Doświadczenie dewelopera i ekosystem

  • GORM: największa społeczność, wiele przykładów/wtyczek; steeper learning curve dla zaawansowanych wzorców.
  • Ent: świetne dokumenty; krok generowania kodu jest głównym zmianą modelu myślenia; bardzo przyjazne dla refaktoryzacji.
  • Bun: czytelne, przewidywalne zapytania; mniejsza, ale aktywna społeczność; świetne funkcje Postgres.
  • sqlc: minimalne zależności czasu wykonywania; dobrze integruje się z narzędziami migracji i CI; doskonałe dla zespołów zgodnych z SQL.

Wyróżnione funkcje

  • Relacje i ładowanie wsteczne: wszystkie obsługują relacje; GORM (tagi + Preload/Joins), Ent (krawędzie + .With...()), Bun (Relation(...)), sqlc (pisz łączenia SQL).
  • Migracje: GORM (auto-migracja; ostrożnie w produkcji), Ent (wygenerowany SQL), Bun (bun/migrate), sqlc (narzędzia zewnętrzne).
  • Hooki/rozszerzalność: GORM (callbacki/wtyczki), Ent (hooki/middleware + szablony/generowanie kodu), Bun (hooki podobne do middleware, łatwe SQL surowe), sqlc (składaj w warstwie aplikacji).
  • JSON/Tablice (Postgres): Bun i GORM mają pomocniki; Ent/sqlc obsługują poprzez typy niestandardowe lub SQL.

Kiedy wybrać co

  • Wybierz GORM, jeśli chcesz maksymalną wygodę, bogate funkcje i szybkie prototypowanie dla konwencjonalnych usług CRUD.
  • Wybierz Ent, jeśli wartość bezpieczeństwa czasu kompilacji, jawnych schematów i długofalowej utrzywalności w większych zespołach.
  • Wybierz Bun, jeśli chcesz wydajność i jawne zapytania SQL z komfortem ORM tam, gdzie pomaga.
  • Wybierz sqlc, jeśli (i twój zespół) preferuje czysty SQL z typowo bezpiecznymi wiązaniami Go i zero narzutu czasu wykonywania.

Minimalny docker-compose.yml dla lokalnego 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

Pakiety i biblioteki ORM w GO

Inne przydatne linki