Een vergelijking van Go-ORMs voor PostgreSQL: GORM vs Ent vs Bun vs sqlc

Praktisch, code-intensief overzicht van ORMs in GO ```

Inhoud

De meest prominente ORMs voor GO zijn GORM, Ent, Bun en sqlc. Hieronder volgt een kleine vergelijking van hen met voorbeelden van CRUD-acties in zuivere GO.

golang + postgresql

TL;DR

  • GORM: veel functies en handig; makkelijkst om “direct te implementeren”, maar heeft meer runtime-overhead.
  • Ent: schema als code met gegenereerde, typeveilige API’s; uitstekend voor grote codebases en herschikkingen.
  • Bun: lichtgewicht, SQL-georiënteerd querybuilder/ORM; snel met uitstekende Postgres-functies, expliciet ontworpen.
  • sqlc (niet echt een ORM, maar wel): schrijf SQL, krijg typeveilige Go; beste ruwe prestaties en controle, geen runtime-magie.

Selectiekriteria en snelle vergelijking

Mijn criteria zijn:

  • Prestaties: latentie/doorvoer, vermijdbare overhead, batch-acties.
  • Gebruikerservaring: leercurve, typeveiligheid, debugbaarheid, codegen-wrijving.
  • Ecosysteem: documentatie, voorbeelden, activiteit, integraties (migraties, tracing).
  • Functieverzameling: relaties, eager loading, migraties, hooks, escapehatches voor ruwe SQL.
Tool Paradigma Typeveiligheid Relaties Migraties Ruwe SQL-ergonomie Typische gebruikscase
GORM Active Record-stijl ORM Gemiddeld (runtime) Ja (tags, Preload/Joins) Automigreren (opt-in) db.Raw(...) Snel implementeren, rijke functies, conventionele CRUD-apps
Ent Schema → codegen → fluïde API Hoog (compile-time) Eersteklas (edges) Gegenereerde SQL (afzonderlijk stap) entsql, aangepaste SQL Grote codebases, herschikkingen-gevoelige teams, strikte typen
Bun SQL-georiënteerd querybuilder/ORM Gemiddeld–Hoog Expliciet (Relation) Afzonderlijk migratiepakket Natuurlijk (builder + ruw) Prestatiebewuste diensten, Postgres-functies
sqlc SQL → codegen functies (niet een ORM) Hoog (compile-time) Via SQL joins Externe tool (bijv. golang-migrate) Het is SQL Maximaal controle & snelheid; DBA-vriendelijke teams

CRUD via voorbeeld

Instellen (PostgreSQL)

Gebruik pgx of het native PG-drijver van het hulpprogramma. Voorbeeld DSN:

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

Imports (algemeen voor alle ORMs)

Aan het begin van elk bestand met go code voorbeeld voeg toe:

import (
  "context"
  "os"
)

We zullen een eenvoudige users-tabel modelleren:

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

GORM

Init

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

// Automigreren (optioneel; wees voorzichtig in productie)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }

CRUD

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

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

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

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

  return nil
}

Opmerkingen

  • Relaties via struct-tags + Preload/Joins.
  • Transactiehelper: db.Transaction(func(tx *gorm.DB) error { ... }).

Ent

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

Genereer code

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

Init

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

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

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

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

  return nil
}

Opmerkingen

  • Sterke typen van eind tot eind; edges voor relaties.
  • Gegenereerde migraties of gebruik uw favoriete migratiehulpmiddel.

Bun

Init

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

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

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

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

  return nil
}

Opmerkingen

  • Expliciete joins/eager loading met .Relation("...").
  • Afzonderlijk bun/migrate-pakket voor migraties.

sqlc

sqlc is geen ORM. U schrijft SQL; het genereert typeveilige 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" # of "github.com/jackc/pgx/v5"

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

Genereer

sqlc generate

Gebruik

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)

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

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

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

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

  return nil
}

Opmerkingen

  • Breng uw eigen migraties mee (bijv. golang-migrate).
  • Voor dynamische queries: schrijf meerdere SQL-varianten of combineer met een klein builder.

Prestatieopmerkingen

  • GORM: handig maar voegt reflectie/abstractie overhead toe. Goed voor typische CRUD; let op N+1 queries (voorkeur voor Joins of selectieve Preload).
  • Ent: gegenereerde code vermijdt reflectie; goed voor complexe schema’s. Vaak sneller dan zware, runtime-magie ORMs.
  • Bun: dun over database/sql; snel, expliciet, uitstekend voor batch-acties en grote resultatenverzamelingen.
  • sqlc: essentieel ruwe SQL-prestaties met compile-time veiligheid.

Algemene tips

  • Gebruik pgx als drijver (v5) en context overal.
  • Voorkeur geven aan batchverwerking (COPY, meervoudige rij INSERT) voor hoge doorvoer.
  • Profielen SQL: EXPLAIN ANALYZE, indexen, covering indexen, vermijd onnodige rondreizen.
  • Verwerk verbindingen; stel poolgrootte in op basis van werkbelasting.

Ontwikkelaarservaring en ecosysteem

  • GORM: grootste gemeenschap, veel voorbeelden/plugins; steile leercurve voor geavanceerde patronen.
  • Ent: uitstekende documentatie; codegen-stap is de hoofd mentale modelshift; super herschikking-vriendelijk.
  • Bun: leesbare, voorspelbare queries; kleinere maar actieve gemeenschap; uitstekende Postgres-nicaties.
  • sqlc: minimale runtime-afhankelijkheden; integreert goed met migratiehulpmiddelen en CI; uitstekend voor teams die comfortabel zijn met SQL.

Functieoverzicht

  • Relaties & eager loading: allemaal relaties verwerken; GORM (tags + Preload/Joins), Ent (edges + .With...()), Bun (Relation(...)), sqlc (u schrijft de joins).
  • Migraties: GORM (automigreren; wees voorzichtig in productie), Ent (gegenereerde/diff SQL), Bun (bun/migrate), sqlc (externe tools).
  • Hooks/Extensibiliteit: GORM (callbacks/plugins), Ent (hooks/middleware + template/codegen), Bun (middleware-achtige queryhooks, eenvoudige ruwe SQL), sqlc (componeren in uw app-laag).
  • JSON/Arrays (Postgres): Bun en GORM hebben handige helpers; Ent/sqlc verwerken via aangepaste typen of SQL.

Wanneer wat kiezen

  • Kies GORM als u maximale handigheid, rijke functies en snelle prototyping voor conventionele CRUD-diensten wilt.
  • Kies Ent als u compile-time veiligheid, expliciete schema’s en langdurige onderhoudbaarheid in grotere teams waardeert.
  • Kies Bun als u prestaties en expliciete SQL-gevormde queries met ORM-comforten waar het helpt wilt.
  • Kies sqlc als u (en uw team) voorkeur geeft aan zuivere SQL met typeveilige Go-bindingen en nul runtime-overhead.

Minimal docker-compose.yml voor 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-pakketten en bibliotheken in GO