Confronto tra gli ORM Go per PostgreSQL: GORM vs Ent vs Bun vs sqlc

Un approfondimento pratico e ricco di codice sugli ORM in GO

Indice

Gli ORM più diffusi per GO sono GORM, Ent, Bun e sqlc. Ecco un breve confronto tra di essi con esempi di operazioni CRUD in puro GO.

golang + postgresql

TL;DR

  • GORM: ricco di funzionalità e comodo; il più semplice per “spedire subito” il codice, ma ha un overhead di runtime maggiore.
  • Ent: schema-as-code con API generate e type-safe; eccellente per codebase ampie e refactor.
  • Bun: leggero, query builder/ORM SQL-first; veloce con ottime funzionalità Postgres, esplicito per design.
  • sqlc (non è un ORM in senso stretto, ma): scrivi SQL, ottieni Go type-safe; migliori prestazioni raw e controllo, nessuna magia al runtime.

Criteri di Selezione e Confronto Rapido

I miei criteri sono:

  • Prestazioni: latenza/throughput, overhead evitabile, operazioni batch.
  • DX: curva di apprendimento, sicurezza dei tipi, debuggabilità, attrito nella codegen.
  • Ecosistema: documentazione, esempi, attività, integrazioni (migrations, tracing).
  • Set di funzionalità: relazioni, eager loading, migrazioni, hook, vie di fuga per SQL raw.
Strumento Paradigma Sicurezza dei Tipi Relazioni Migrazioni Ergonomia SQL Raw Caso d’Uso Tipico
GORM ORM stile Active Record Media (runtime) Sì (tag, Preload/Joins) Auto-migrate (opt-in) db.Raw(...) Consegna rapida, funzionalità ricche, app CRUD convenzionali
Ent Schema → codegen → API fluent Alta (compilation-time) Prima classe (edges) SQL generato (passo separato) entsql, SQL custom Codebase ampi, team con molti refactor, tipizzazione rigorosa
Bun Query builder/ORM SQL-first Media–Alta Esplicito (Relation) Pacco di migrazione separato Naturale (builder + raw) Servizi attenti alle prestazioni, funzionalità Postgres
sqlc SQL → funzioni codegen (non un ORM) Alta (compilation-time) Tramite join SQL Strumento esterno (es. golang-migrate) È il SQL Massimo controllo e velocità; team amichevoli ai DBA

CRUD con Esempi

Configurazione (PostgreSQL)

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

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

Import (comuni a tutti gli ORM)

All’inizio di ogni file con codice go 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 delle 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(),
  }
}

Generazione del 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 le relazioni.
  • Migrazioni generate o usa il tuo tool 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 tecnicamente non è un ORM. Scrivi SQL; genera metodi Go type-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" # or "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;

Generazione

sqlc generate

Uso

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

  • Portare le proprie migrazioni (es. golang-migrate).
  • Per query dinamiche: scrivere più varianti SQL o combinare con un piccolo builder.

Note sulle Prestazioni

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

Consigli generali

  • Usa pgx per il driver (v5) e context ovunque.
  • Preferisci il batching (COPY, INSERT multi-riga) per alto throughput.
  • Profila il SQL: EXPLAIN ANALYZE, indici, indici coprenti, evita roundtrips non necessari.
  • Riusa le connessioni; regola la dimensione del pool in base al carico di lavoro.

Esperienza dello Sviluppatore ed Ecosistema

  • GORM: community più grande, tanti esempi/plugin; curva di apprendimento più ripida per pattern avanzati.
  • Ent: ottima documentazione; il passo della codegen è la principale svolta mentale; molto amichevole per i refactor.
  • Bun: query leggibili e prevedibili; community più piccola ma attiva; ottime funzionalità Postgres.
  • sqlc: dipendenze runtime minime; si integra bene con tool di migrazione e CI; eccellente per team a loro agio con SQL.

Funzionalità in Evidenza

  • Relazioni ed eager loading: tutti gestiscono le relazioni; GORM (tag + Preload/Joins), Ent (edges + .With...()), Bun (Relation(...)), sqlc (scrivi tu i join).
  • Migrazioni: GORM (auto-migrate; attenzione in prod), Ent (SQL generato/diff), Bun (bun/migrate), sqlc (strumenti esterni).
  • Hook/Estendibilità: GORM (callback/plugin), Ent (hook/middleware + template/codegen), Bun (hook di query simili a middleware, SQL raw facile), sqlc (componi nel tuo layer applicativo).
  • JSON/Array (Postgres): Bun e GORM hanno helper comodi; Ent/sqlc gestiscono tramite tipi custom o SQL.

Una nota trasversale per tutti e quattro: Go 1.27 ora supporta encoding/json con il motore v2, che preserva il comportamento ma cambia alcuni testi di errore e, sotto l’API v2 esplicita, l’output per slice nil e map-key. Se i tuoi modelli fanno round-trip JSON, controlla cosa dice la guida all’upgrade Go 1.27 sui fixture dei test d’oro prima di aggiornare.

Quando Scegliere Cosa

  • Scegli GORM se vuoi massima comodità, funzionalità ricche e prototipazione rapida per servizi CRUD convenzionali.
  • Scegli Ent se valorizzi la sicurezza in fase di compilazione, schemi espliciti e manutenibilità a lungo termine in team più grandi.
  • Scegli Bun se vuoi prestazioni e query esplicite con forma SQL con i comfort degli ORM dove aiutano.
  • Scegli sqlc se (e il tuo team) preferite il puro SQL con binding Go type-safe e zero overhead runtime. sqlc è anche una scelta naturale per il lato read model di un’architettura CQRS in Go dove le query sono formate per i chiamanti piuttosto che per le entità di dominio e il SQL esplicito ti dà pieno controllo sulla proiezione.

Se stai ancora bilanciando la scelta di questo ORM rispetto allo stile di integrazione e ai confini dei servizi, questa panoramica sull’architettura dell’applicazione aiuta a posizionare la decisione in un contesto di produzione più ampio.


docker-compose.yml Minimo 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 per GO

Iscriviti

Ricevi nuovi articoli su sistemi, infrastruttura e ingegneria AI.