Confronto degli ORM per PostgreSQL in Go: GORM vs Ent vs Bun vs sqlc

Una panoramica pratica e ricca di codice sugli ORM in GO

Indice

I framework ORM più utilizzati per GO sono GORM, Ent, Bun e sqlc. Ecco un piccolo confronto tra di loro con esempi di operazioni CRUD in GO puro.

golang + postgresql

TL;DR

  • GORM: ricco di funzionalità e conveniente; più semplice da “lanciare”, ma ha un maggiore overhead in fase di esecuzione.
  • Ent: schema come codice con API generate e tipo-safe; eccellente per grandi codici e ristrutturazioni.
  • Bun: leggero, costruttore di query SQL-first/ORM; veloce con ottime funzionalità per Postgres, esplicito per design.
  • sqlc (non è un ORM ma comunque): scrivi SQL, ottieni Go tipo-safe; migliore prestazione e controllo raw, nessun magia in fase di esecuzione.

Criteri di selezione e confronto rapido

I miei criteri sono:

  • Prestazioni: latenza/throughput, overhead evitabile, operazioni batch.
  • DX: curva di apprendimento, sicurezza tipo, debuggabilità, attrito codegen.
  • Ecosistema: documentazione, esempi, attività, integrazioni (migrazioni, tracciamento).
  • Set di funzionalità: relazioni, caricamento eager, migrazioni, hook, escape hatches SQL raw.
Strumento Paradigma Sicurezza tipo Relazioni Migrazioni Ergonomia SQL raw Caso d’uso tipico
GORM ORM stile Active Record Media (runtime) Sì (tag, Preload/Joins) Auto-migrate (opzionale) db.Raw(...) Consegna rapida, funzionalità ricche, app CRUD convenzionali
Ent Schema → codegen → API fluida Alta (compile-time) Prima classe (edges) SQL generato (passo separato) entsql, SQL personalizzato Codici grandi, team con molte ristrutturazioni, tipizzazione rigorosa
Bun Costruttore di query SQL-first/ORM Media–Alta Esplicito (Relation) Pacchetto separato per migrazioni Naturale (builder + raw) Servizi orientati alle prestazioni, funzionalità Postgres
sqlc SQL → funzioni generate (non è un ORM) Alta (compile-time) Attraverso join SQL Strumento esterno (es. golang-migrate) è SQL Controllo e velocità massimi; team amici dei DBA

CRUD con esempi

Configurazione iniziale (PostgreSQL)

Utilizza pgx o il driver nativo PG dello strumento. Esempio DSN:

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

Importazioni (comuni a tutti gli ORM)

All’inizio di ogni file con go code esempio aggiungi:

import (
  "context"
  "os"
)

Modelleremo una semplice tabella users:

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

GORM

Inizializzazione

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

CRUD

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

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

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

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

  return nil
}

Note

  • Relazioni tramite tag struct + Preload/Joins.
  • Helper per transazioni: db.Transaction(func(tx *gorm.DB) error { ... }).

Ent

Definizione dello schema (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(),
  }
}

Genera codice

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

Inizializzazione

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

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

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

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

  return nil
}

Note

  • Tipizzazione forte end-to-end; edges per relazioni.
  • Migrazioni generate o usa il tuo strumento di migrazione preferito.

Bun

Inizializzazione

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

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

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

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

  return nil
}

Note

  • Join/eager loading espliciti con .Relation("...").
  • Pacchetto separato bun/migrate per le migrazioni.

sqlc

sqlc non è un ORM. Scrivi SQL; genera metodi Go tipo-safe.

sqlc.yaml

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

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

Genera

sqlc generate

Utilizzo

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)

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

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

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

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

  return nil
}

Note

  • Porta le tue migrazioni (es. golang-migrate).
  • Per query dinamiche: scrivi varianti SQL multiple o combina con un piccolo builder.

Note sulle prestazioni

  • GORM: conveniente ma aggiunge overhead di riflessione/abstrazione. Buono per CRUD tipici; attenzione alle query N+1 (preferisci Joins o Preload selettivo).
  • Ent: il codice generato evita la riflessione; buono per schemi complessi. Spesso più veloce degli ORM pesanti con magia in fase di esecuzione.
  • Bun: sottile su database/sql; veloce, esplicito, eccellente per operazioni batch e grandi set di risultati.
  • sqlc: prestazioni essenzialmente raw SQL con sicurezza in fase di compilazione.

Consigli generali

  • Usa pgx come driver (v5) e context ovunque.
  • Preferisci batching (COPY, INSERT multi-row) per alta throughput.
  • Profila SQL: EXPLAIN ANALYZE, indici, indici copertura, evita roundtrip inutili.
  • Riutilizza le connessioni; regola la dimensione del pool in base al carico di lavoro.

Esperienza dello sviluppatore e ecosistema

  • GORM: comunità più grande, molti esempi/plugin; curva di apprendimento più ripida per pattern avanzati.
  • Ent: ottime documentazioni; il passo di codegen è il principale cambiamento mentale; super ristrutturabile.
  • Bun: query leggibili e prevedibili; comunità più piccola ma attiva; eccellenti funzionalità Postgres.
  • sqlc: dipendenze runtime minimali; integra bene con strumenti di migrazione e CI; perfetto per team abituati a SQL.

Punti di forza delle funzionalità

  • Relazioni & caricamento eager: tutti gestiscono relazioni; GORM (tag + Preload/Joins), Ent (edges + .With...()), Bun (Relation(...)), sqlc (scrivi i join).
  • Migrazioni: GORM (auto-migrate; attenzione in produzione), Ent (SQL generato/diff), Bun (bun/migrate), sqlc (strumenti esterni).
  • Hook/estensibilità: GORM (callback/plugin), Ent (hook/middleware + template/codegen), Bun (hook simili a middleware, facile SQL raw), sqlc (componi nell’app layer).
  • JSON/Array (Postgres): Bun e GORM hanno helper utili; Ent/sqlc gestiscono tramite tipi personalizzati o SQL.

Quando scegliere cosa

  • Scegli GORM se desideri la massima convenienza, funzionalità ricche e prototipazione rapida per servizi CRUD convenzionali.
  • Scegli Ent se valori la sicurezza in fase di compilazione, schemi espliciti e manutenibilità a lungo termine in team più grandi.
  • Scegli Bun se desideri prestazioni e query SQL esplicite con comfort ORM dove aiuta.
  • Scegli sqlc se tu e il tuo team preferite SQL puro con binding Go tipo-safe e zero overhead in fase di esecuzione.

Esempio minimo di docker-compose.yml per PostgreSQL locale

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

Pacchetti e librerie ORM in GO