Vergleich von Go-ORMs für PostgreSQL: GORM vs Ent vs Bun vs sqlc

Ein praktischer, codeorientierter Blick auf ORMs in Go

Inhaltsverzeichnis

Die bekanntesten ORMs für GO sind GORM, Ent, Bun und sqlc. Hier ist ein kleiner Vergleich mit Beispielen für CRUD-Operationen in reinem GO.

golang + postgresql

TL;DR

  • GORM: funktionsreich und bequem; am einfachsten zu „versenden“, aber mit mehr Laufzeit-Overhead.
  • Ent (entgo.io): Schema-as-Code mit generierten, typsicheren APIs; ideal für große Codebases und Refactorings.
  • Bun: leichtgewichtig, SQL-first Query Builder/ORM; schnell mit hervorragenden PostgreSQL-Funktionen, explizit durch Design.
  • sqlc (kein ORM): SQL schreiben, typsicheres Go erhalten; beste Rohleistung und Kontrolle, keine Laufzeit-Magie.

Auswahlkriterien und schneller Vergleich

Meine Kriterien sind:

  • Performance: Latenz/Durchsatz, vermeidbarer Overhead, Batch-Operationen.
  • DX: Lernkurve, Typsicherheit, Debuggbarkeit, Codegenerierungsaufwand.
  • Ökosystem: Dokumentation, Beispiele, Aktivität, Integrationen (Migrations, Tracing).
  • Funktionsumfang: Beziehungen, Eager Loading, Migrations, Hooks, Raw SQL Escape Hatches.
Tool Paradigm Typsicherheit Beziehungen Migrations Raw SQL Ergonomie Typischer Anwendungsfall
GORM Active Record-ähnlicher ORM Mittel (Laufzeit) Ja (Tags, Preload/Joins) Auto-migrate (optional) db.Raw(...) Schnelle Lieferung, reichhaltige Funktionen, konventionelle CRUD-Anwendungen
Ent Schema → Codegenerierung → flüssige API Hoch (Kompilierungszeit) Erste Klasse (Edges) Generierte SQL (separater Schritt) entsql, benutzerdefiniertes SQL Große Codebases, refaktorierungsintensive Teams, strikte Typisierung
Bun SQL-first Query Builder/ORM Mittel–Hoch Explizit (Relation) Separates Migrations-Paket Natürlich (Builder + Raw) Leistungsbewusste Dienste, PostgreSQL-Funktionen
sqlc SQL → Codegenerierungsfunktionen (kein ORM) Hoch (Kompilierungszeit) Über SQL-Joins Externes Tool (z. B. golang-migrate) Es ist SQL Maximale Kontrolle & Geschwindigkeit; DBA-freundliche Teams

CRUD am Beispiel

Einrichtung (PostgreSQL)

Verwenden Sie pgx oder den nativen PG-Treiber des Tools. Beispiel-DSN:

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

Imports (gemeinsam für alle ORMs)

Am Anfang jeder Datei mit Go-Code Beispiel hinzufügen:

import (
  "context"
  "os"
)

Wir werden eine einfache users-Tabelle modellieren:

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

GORM

Initialisierung

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-migrate (optional; vorsichtig in Produktion)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }

CRUD

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

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

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

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

  return nil
}

Hinweise

  • Beziehungen über Strukturtags + Preload/Joins.
  • Transaktionshilfe: db.Transaction(func(tx *gorm.DB) error { ... }).

Ent

Schema-Definition (in 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(),
  }
}

Code generieren

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

Initialisierung

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

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

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

  // Löschen
  if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }

  return nil
}

Hinweise

  • Starke Typisierung von Anfang bis Ende; Edges für Beziehungen.
  • Generierte Migrations oder Verwendung Ihres bevorzugten Migrations-Tools.

Bun

Initialisierung

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

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

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

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

  return nil
}

Hinweise

  • Explizite Joins/Eager Loading mit .Relation("...").
  • Separates bun/migrate-Paket für Migrations.

sqlc

sqlc ist kein ORM. Sie schreiben SQL; es generiert typsichere Go-Methoden.

sqlc.yaml

version: "2"
sql:
  - engine: postgresql
    queries: db/queries
    schema: db/migrations
    gen:
      go:
        package: db
        out: internal/db
        sql_package: "database/sql" # oder "github.com/jackc/pgx/v5"

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

Generieren

sqlc generate

Verwendung

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)

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

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

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

  // Löschen
  if err := q.DeleteUser(ctx, up.ID); err != nil { return err }

  return nil
}

Hinweise

  • Bringen Sie Ihre eigenen Migrations mit (z. B. golang-migrate).
  • Für dynamische Abfragen: Schreiben Sie mehrere SQL-Varianten oder kombinieren Sie sie mit einem kleinen Builder.

Performance-Hinweise

  • GORM: bequem, aber fügt Reflexions-/Abstraktions-Overhead hinzu. Gut für typische CRUD-Anwendungen; achten Sie auf N+1-Abfragen (bevorzugen Sie Joins oder selektives Preload).
  • Ent: generierter Code vermeidet Reflexion; gut für komplexe Schemas. Oft schneller als schwere, Laufzeit-Magie-ORMs.
  • Bun: dünn über database/sql; schnell, explizit, ideal für Batch-Operationen und große Ergebnisgruppen.
  • sqlc: im Wesentlichen Roh-SQL-Leistung mit Kompilierungszeit-Sicherheit.

Allgemeine Tipps

  • Verwenden Sie pgx als Treiber (Version 5) und context überall.
  • Bevorzugen Sie Batch-Operationen (COPY, mehrzeilige INSERT) für hohen Durchsatz.
  • Profilen Sie SQL: EXPLAIN ANALYZE, Indizes, abdeckende Indizes, vermeiden Sie unnötige Roundtrips.
  • Wiederverwenden Sie Verbindungen; passen Sie die Poolgröße basierend auf der Arbeitslast an.

Entwicklererfahrung und Ökosystem

  • GORM: größte Community, viele Beispiele/Plugins; steilere Lernkurve für fortgeschrittene Muster.
  • Ent: hervorragende Dokumentation; Codegenerierungsschritt ist die Hauptgedankenverschiebung; super refaktorierungsfreundlich.
  • Bun: lesbare, vorhersehbare Abfragen; kleinere, aber aktive Community; exzellente PostgreSQL-Features.
  • sqlc: minimale Laufzeitabhängigkeiten; integriert sich gut mit Migrationswerkzeugen und CI; hervorragend für Teams, die sich mit SQL wohlfühlen.

Hervorhebungen

  • Relationen & Eager Loading: alle handhaben Relationen; GORM (Tags + Preload/Joins), Ent (Kanten + .With...()), Bun (Relation(...)), sqlc (Sie schreiben die Joins).
  • Migrationen: GORM (Auto-Migration; vorsichtig in Produktion), Ent (generierte/diff SQL), Bun (bun/migrate), sqlc (externe Tools).
  • Hooks/Erweiterbarkeit: GORM (Callbacks/Plugins), Ent (Hooks/Middleware + Template/Codegenerierung), Bun (middleware-ähnliche Abfrage-Hooks, einfacher roher SQL-Zugriff), sqlc (Zusammensetzen in Ihrer Anwendungsebene).
  • JSON/Arrays (PostgreSQL): Bun und GORM haben nette Helfer; Ent/sqlc handhaben über benutzerdefinierte Typen oder SQL.

Wann was wählen

  • Wählen Sie GORM, wenn Sie maximale Bequemlichkeit, reichhaltige Funktionen und schnelles Prototyping für konventionelle CRUD-Dienste wünschen.
  • Wählen Sie Ent, wenn Ihnen Compile-Time-Sicherheit, explizite Schemas und langfristige Wartbarkeit in größeren Teams am Herzen liegen.
  • Wählen Sie Bun, wenn Sie Leistung und explizit SQL-förmige Abfragen mit ORM-Komforts möchten, wo es hilft.
  • Wählen Sie sqlc, wenn Sie (und Ihr Team) reines SQL mit typsicheren Go-Bindings und null Laufzeit-Overhead bevorzugen.

Minimales docker-compose.yml für lokale 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-Pakete und -Bibliotheken in GO