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.
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 selektivesPreload
). - 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
, mehrzeiligeINSERT
) 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