Jämförelse av Go-ORM:er för PostgreSQL: GORM vs Ent vs Bun vs sqlc

En praktisk, kodintensiv titt på ORM:er i Go

Sidinnehåll

De mest framträdande ORM:erna för Go är GORM, Ent, Bun och sqlc. Här är en liten jämförelse av dem med exempel på CRUD-operationer i ren Go.

golang + postgresql

TL;DR

  • GORM: Funktionssam och bekväm; enklast att “bara släppa”, men har mer körtidsoverhead.
  • Ent: Schema-as-code med genererade, typsäkra API:er; utmärkt för stora kodbasar och refaktorer.
  • Bun: Lättviktig, SQL-first query builder/ORM; snabb med utmärkta Postgres-funktioner, explicit by design.
  • sqlc (inte en ORM per se, men ändå): Skriv SQL, få typsäkert Go; bäst rå prestanda och kontroll, ingen magi vid körtid.

Urvalskriterier och snabb jämförelse

Mina kriterier är:

  • Prestanda: Latens/genomströmning, undvikbar overhead, batchoperationer.
  • Utvecklarupplevelse (DX): Inlärningskurva, typsäkerhet, felsäkerhet, kodgenereringsfriktion.
  • Ekosystem: Dokumentation, exempel, aktivitet, integrationer (migrationer, tracing).
  • Funktionsset: Relationer, eager loading, migrationer, hooks, escape-hatch för raw SQL.
Verktyg Paradigm Typsäkerhet Relationer Migrationer Ergonomi för raw SQL Typiskt användningsfall
GORM Active Record-stilad ORM Medium (vid körtid) Ja (tags, Preload/Joins) Auto-migrate (opt-in) db.Raw(...) Snabb leverans, rika funktioner, konventionella CRUD-appar
Ent Schema → kodgenerering → flödes-API Hög (vid kompilering) First-class (edges) Genererad SQL (separat steg) entsql, anpassad SQL Stora kodbasar, team som refaktorerar mycket, strikt typning
Bun SQL-first query builder/ORM Medium–Hög Explicit (Relation) Separat migrate-paket Naturlig (builder + raw) Prestandabevätna tjänster, Postgres-funktioner
sqlc SQL → kodgenererade funktioner (inte en ORM) Hög (vid kompilering) Via SQL-joiner Externt verktyg (t.ex., golang-migrate) Det är SQL Maximal kontroll & hastighet; DBA-vänliga team

CRUD med exempel

Uppstart (PostgreSQL)

Använd pgx eller verktygets inbyggda PG-drivrutin. Exempel DSN:

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

Importeringar (gemensamma för alla ORM:er)

I början av varje fil med go-kod exempel lägg till:

import (
  "context"
  "os"
)

Vi modellerar en enkel users-tabell:

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

// Auto-migrate (valfritt; var försiktig i produktion)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }

CRUD

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

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

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

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

  return nil
}

Noter

  • Relationer via struct-tagg + Preload/Joins.
  • Transaktionshjälp: db.Transaction(func(tx *gorm.DB) error { ... }).

Ent

Schemadefinition (i 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(),
  }
}

Generera kod

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

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

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

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

  return nil
}

Noter

  • Stark typning end-to-end; edges för relationer.
  • Genererade migrationer eller använd ditt valda migrationsverktyg.

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

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

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

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

  return nil
}

Noter

  • Explicita joiner/eager loading med .Relation("...").
  • Separat bun/migrate-paket för migrationer.

sqlc

sqlc är tekniskt sett inte en ORM. Du skriver SQL; den genererar typsäkra Go-metoder.

sqlc.yaml

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

Frågor (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;

Generera

sqlc generate

Användning

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)

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

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

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

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

  return nil
}

Noter

  • Ta med dina egna migrationer (t.ex., golang-migrate).
  • För dynamiska frågor: skriv flera SQL-varianten eller kombinera med en liten builder.

Prestandanoter

  • GORM: Bekväm men lägger till reflektion/abstraktionsoverhead. Passar bra för typisk CRUD; se upp efter N+1-frågor (föredra Joins eller selektiv Preload).
  • Ent: Genererad kod undviker reflektion; bra för komplexa schema. Ofta snabbare än tunga, körtidsmagiska ORM:er.
  • Bun: Tunna över database/sql; snabb, explicit, bra för batchoperationer och stora resultatset.
  • sqlc: Essentiellt sett raw SQL-prestanda med typsäkerhet vid kompilering.

Allmänna tips

  • Använd pgx som drivrutin (v5) och context överallt.
  • Föredra batchning (COPY, multi-row INSERT) för hög genomströmning.
  • Profiler SQL: EXPLAIN ANALYZE, index, covering index, undvik onödiga roundtrips.
  • Återanvänd connectioner; justera poolstorlek baserat på arbetsbelastning.

Utvecklarupplevelse och ekosystem

  • GORM: Största community, många exempel/plugins; brantare inlärningskurva för avancerade mönster.
  • Ent: Bra dokumentation; kodgenereringssteget är den huvudsakliga mentala modellförändringen; super refactor-vänlig.
  • Bun: Läsbara, förutsägbara frågor; mindre men aktivt community; utmärkta Postgres-niceties.
  • sqlc: Minimala körtidsberoenden; integreras bra med migrationsverktyg och CI; superb för team som är bekväma med SQL.

Funktionshöjdpunkter

  • Relationer & eager loading: Alla hanterar relationer; GORM (tags + Preload/Joins), Ent (edges + .With...()), Bun (Relation(...)), sqlc (du skriver joinerna).
  • Migrationer: GORM (auto-migrate; var försiktig i produktion), Ent (genererad/diff SQL), Bun (bun/migrate), sqlc (externa verktyg).
  • Hooks/Extensibilitet: GORM (callbacks/plugins), Ent (hooks/middleware + template/kodgenerering), Bun (middleware-liknande query hooks, enkel raw SQL), sqlc (komposera i din app-lager).
  • JSON/Arrays (Postgres): Bun och GORM har fina hjälpmedel; Ent/sqlc hanterar via anpassade typer eller SQL.

När du väljer vad

  • Välj GORM om du vill ha maximal bekvämlighet, rika funktioner och snabb prototypering för konventionella CRUD-tjänster.
  • Välj Ent om du värdesätter typsäkerhet vid kompilering, explicita schema och långsiktig underhållbarhet i större team.
  • Välj Bun om du vill ha prestanda och explicita SQL-formade frågor med ORM-bekvämligheter där det hjälper.
  • Välj sqlc om du (och ditt team) föredrar ren SQL med typsäkra Go-bindningar och noll körtidsoverhead. sqlc är också en naturlig passform för lässidan av en CQRS-arkitektur i Go, där frågor formades för anropare snarare än domänentiteter och explicit SQL ger dig full kontroll över projectionen.

Om du fortfarande väger detta ORM-val mot integrationsstil och tjänstegransser, den här app-arkitekturöversikten hjälper till att placera beslutet i ett bredare produktionskontext.


Minimal docker-compose.yml för lokal 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-paket och bibliotek i Go

Andra användbara länkar

Prenumerera

Få nya inlägg om system, infrastruktur och AI-ingenjörskonst.