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

Ein praktischer, codeintensiver Blick auf ORMs in Go

Inhaltsverzeichnis

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

golang + postgresql

TL;DR

  • GORM: funktionsreich und komfortabel; am einfachsten „loslegen“, aber mit höherem Laufzeit-Overhead.
  • Ent: Schema-as-Code mit generierten, typsicheren APIs; ausgezeichnet für große Codebasen und Refactorings.
  • Bun: schlanker, SQL-first Query Builder/ORM; schnell mit großartigen Postgres-Funktionen, explizit durch Design.
  • sqlc (kein ORM im eigentlichen Sinne, aber dennoch): Schreiben Sie SQL, erhalten Sie typsicheres Go; beste rohe Leistung und Kontrolle, kein Laufzeit-Magic.

Auswahlkriterien und schneller Vergleich

Meine Kriterien sind:

  • Leistung: Latenz/Durchsatz, vermeidbarer Overhead, Batch-Operationen.
  • DX (Developer Experience): Lernkurve, Typsicherheit, Debuggbarkeit, Reibungsverluste bei der Codegenerierung.
  • Ökosystem: Dokumentation, Beispiele, Aktivität, Integrationen (Migrationen, Tracing).
  • Funktionsumfang: Beziehungen, Eager Loading, Migrationen, Hooks, Ausweichmöglichkeiten für rohes SQL.
Werkzeug Paradigma Typsicherheit Beziehungen Migrationen Ergonomie rohen SQLs Typischer Anwendungsfall
GORM Active-Record-Style ORM Mittel (Laufzeit) Ja (Tags, Preload/Joins) Auto-migrate (opt-in) db.Raw(...) Schnelle Entwicklung, reiche Features, konventionelle CRUD-Apps
Ent Schema → Codegen → fluent API Hoch (Compile-time) Erster Klasse (Edges) Generiertes SQL (separater Schritt) entsql, benutzerdefiniertes SQL Große Codebasen, refactoringstarke Teams, strikte Typisierung
Bun SQL-first Query Builder/ORM Mittel–Hoch Explizit (Relation) Separates Migrate-Package Natürlich (Builder + raw) Leistungsbewusste Services, Postgres-Features
sqlc SQL → Codegen-Funktionen (kein ORM) Hoch (Compile-time) Ü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 Werkzeugs. Beispiel-DNS:

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

Imports (gemeinsam für alle ORMs)

Fügen Sie am Anfang jeder Datei mit Go-Code-Beispielen Folgendes hinzu:

import (
  "context"
  "os"
)

Wir modellieren eine einfache users-Tabelle:

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; Vorsicht in der 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 Struct-Tags + Preload/Joins.
  • Transaktions-Helfer: db.Transaction(func(tx *gorm.DB) error { ... }).

Ent

Schemadefinition (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

  • Strikte Typisierung durchgängig; Edges für Beziehungen.
  • Generierte Migrationen oder verwenden Sie Ihr bevorzugtes Migrationstool.

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-Package für Migrationen.

sqlc

sqlc ist technisch gesehen 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

Nutzung

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 Migrationen mit (z. B. golang-migrate).
  • Für dynamische Abfragen: Schreiben Sie mehrere SQL-Varianten oder kombinieren Sie sie mit einem kleinen Builder.

Leistungsaspekte

  • GORM: Komfortabel, fügt aber Reflection-/Abstraktions-Overhead hinzu. Gut für typisches CRUD; achten Sie auf N+1-Abfragen (bevorzugen Sie Joins oder selektives Preload).
  • Ent: Generierter Code vermeidet Reflection; gut für komplexe Schemata. Oft schneller als ORMs mit viel Laufzeit-Magic.
  • Bun: Dünne Schicht über database/sql; schnell, explizit, großartig für Batch-Operationen und große Ergebnismengen.
  • sqlc: Im Wesentlichen die Leistung von rohem SQL mit Compile-Time-Sicherheit.

Allgemeine Tipps

  • Verwenden Sie pgx als Treiber (v5) und context überall.
  • Bevorzugen Sie Batches (COPY, multi-row INSERT) für hohen Durchsatz.
  • Profilen Sie SQL: EXPLAIN ANALYZE, Indizes, Covering-Indizes, vermeiden Sie unnötige Roundtrips.
  • Reuse connections; passen Sie die Pool-Größe an die Arbeitslast an.

Developer Experience und Ökosystem

  • GORM: Größte Community, viele Beispiele/Plugins; steilere Lernkurve für fortgeschrittene Muster.
  • Ent: Großartige Dokumentation; der Codegen-Schritt ist der Hauptwechsel im Denkmodell; super refactor-freundlich.
  • Bun: Lesbare, vorhersagbare Abfragen; kleinere, aber aktive Community; ausgezeichnete Postgres-Features.
  • sqlc: Minimale Laufzeit-Abhängigkeiten; integriert sich gut mit Migrationstools und CI; hervorragend für Teams, die mit SQL vertraut sind.

Funktionshervorhebungen

  • Beziehungen & Eager Loading: Alle behandeln Beziehungen; GORM (Tags + Preload/Joins), Ent (Edges + .With...()), Bun (Relation(...)), sqlc (Sie schreiben die Joins).
  • Migrationen: GORM (Auto-migrate; Vorsicht in der Produktion), Ent (generiertes/diff SQL), Bun (bun/migrate), sqlc (externe Tools).
  • Hooks/Erweiterbarkeit: GORM (Callbacks/Plugins), Ent (Hooks/Middleware + Template/Codegen), Bun (Middleware-ähnliche Query-Hooks, einfaches rohes SQL), sqlc (Zusammenstellung in Ihrer App-Schicht).
  • JSON/Arrays (Postgres): Bun und GORM haben nette Helfer; Ent/sqlc behandeln dies über benutzerdefinierte Typen oder SQL.

Was wann wählen

  • Wählen Sie GORM, wenn Sie maximalen Komfort, reiche Features und schnelles Prototyping für konventionelle CRUD-Services wünschen.
  • Wählen Sie Ent, wenn Sie Compile-Time-Sicherheit, explizite Schemata und langfristige Wartbarkeit in größeren Teams schätzen.
  • Wählen Sie Bun, wenn Sie Leistung und explizite SQL-geformte Abfragen mit ORM-Bequemlichkeiten dort, wo es hilft, möchten.
  • Wählen Sie sqlc, wenn Sie (und Ihr Team) reines SQL mit typsicheren Go-Bindings und Null-Laufzeit-Overhead bevorzugen. sqlc ist auch eine natürliche Passform für die Lese-Modell-Seite einer CQRS-Architektur in Go, wo Abfragen für Aufrufer geformt werden, anstatt für Domain-Entitäten, und explizites SQL Ihnen volle Kontrolle über die Projektion gibt.

Wenn Sie diese ORM-Entwahl noch gegen Integrationsstil und Service-Grenzen abwägen, hilft dieser Überblick zur App-Architektur, um die Entscheidung in einen breiteren Produktionskontext einzuordnen.


Minimales docker-compose.yml für lokales 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-Packages und Bibliotheken in Go

Abonnieren

Neue Beiträge zu Systemen, Infrastruktur und KI-Engineering.