Comparaison des ORMs Go pour PostgreSQL : GORM vs Ent vs Bun vs sqlc

Un aperçu pratique et axé sur le code des ORMs en GO ```

Sommaire

Les ORM les plus réputés pour GO sont GORM, Ent, Bun et sqlc. Voici une petite comparaison d’entre eux avec des exemples d’opérations CRUD en pure GO.

golang + postgresql

TL;DR

  • GORM : fonctionnalités nombreuses et pratiques ; le plus facile à « simplement déployer », mais avec un surcoût d’exécution plus important.
  • Ent : schéma en code avec des API générées et typées ; idéal pour les grands projets et les refactorings.
  • Bun : léger, générateur de requêtes SQL-first/ORM ; rapide avec de bonnes fonctionnalités pour Postgres, explicite par conception.
  • sqlc (non un ORM au sens strict mais tout de même) : écrivez du SQL, obtenez des fonctions Go typées ; meilleure performance brute et contrôle, pas de magie d’exécution.

Critères de sélection et comparaison rapide

Mes critères sont :

  • Performance : latence/throughput, surcoût évitable, opérations par lots.
  • DX : courbe d’apprentissage, sécurité des types, débogabilité, friction de la génération de code.
  • Écosystème : documentation, exemples, activité, intégrations (migrations, traçage).
  • Ensemble de fonctionnalités : relations, chargement anticipé, migrations, hooks, sorties SQL brutes.
Outil Paradigme Sécurité des types Relations Migrations Ergonomie du SQL brut Cas d’utilisation typique
GORM ORM de type Active Record Moyenne (runtime) Oui (tags, Preload/Joins) Auto-migrate (optionnel) db.Raw(...) Livraison rapide, fonctionnalités riches, applications CRUD conventionnelles
Ent Schéma → génération de code → API fluide Élevée (compile-time) Première classe (edges) SQL généré (étape séparée) entsql, SQL personnalisé Grandes bases de code, équipes en phase de refactor, typage strict
Bun Générateur de requêtes SQL-first/ORM Moyenne–Élevée Explicite (Relation) Package séparé pour les migrations Naturel (constructeur + brut) Services sensibles à la performance, fonctionnalités Postgres
sqlc SQL → génération de fonctions (non un ORM) Élevée (compile-time) Via les jointures SQL Outil externe (ex. golang-migrate) C’est le SQL Contrôle et vitesse maximum ; équipes amicales aux DBA

CRUD par exemple

Configuration (PostgreSQL)

Utilisez pgx ou le pilote natif PG de l’outil. Exemple de DSN :

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

Importations (communes à tous les ORMs)

Au début de chaque fichier avec go code exemple ajoutez :

import (
  "context"
  "os"
)

Nous modéliserons une table simple users :

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

GORM

Initialisation

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 (optionnel ; faites attention en production)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }

CRUD

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

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

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

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

  return nil
}

Notes

  • Relations via les balises de structure + Preload/Joins.
  • Helper de transaction : db.Transaction(func(tx *gorm.DB) error { ... }).

Ent

Définition du schéma (dans 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(),
  }
}

Générer du code

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

Initialisation

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

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

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

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

  return nil
}

Notes

  • Typage fort de bout en bout ; arêtes pour les relations.
  • Migrations générées ou utilisez votre outil de migration de choix.

Bun

Initialisation

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

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

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

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

  return nil
}

Notes

  • Jointures/explicit loading avec .Relation("...").
  • Package séparé bun/migrate pour les migrations.

sqlc

sqlc n’est techniquement pas un ORM. Vous écrivez du SQL ; il génère des méthodes Go typées.

sqlc.yaml

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

Requêtes (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;

Générer

sqlc generate

Utilisation

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)

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

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

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

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

  return nil
}

Notes

  • Apportez vos propres migrations (ex. golang-migrate).
  • Pour les requêtes dynamiques : écrivez plusieurs variantes SQL ou combinez avec un petit générateur.

Notes sur les performances

  • GORM : pratique mais ajoute un surcoût d’abstraction via la réflexion. Bon pour les CRUD classiques ; faites attention aux requêtes N+1 (privilégiez Joins ou un Preload sélectif).
  • Ent : le code généré évite la réflexion ; bon pour les schémas complexes. Souvent plus rapide que les ORMs lourds avec magie d’exécution.
  • Bun : mince sur database/sql ; rapide, explicite, idéal pour les opérations par lots et les grands ensembles de résultats.
  • sqlc : performance brute du SQL avec une sécurité au moment de la compilation.

Conseils généraux

  • Utilisez pgx comme pilote (v5) et context partout.
  • Privilégiez le regroupement (COPY, INSERT multi-lignes) pour un throughput élevé.
  • Profilage SQL : EXPLAIN ANALYZE, index, index couvrants, évitez les allers-retours inutiles.
  • Réutilisez les connexions ; ajustez la taille du pool en fonction de la charge.

Expérience du développeur et écosystème

  • GORM : communauté la plus grande, nombreux exemples/plugins ; courbe d’apprentissage plus raide pour les modèles avancés.
  • Ent : documentation excellente ; étape de génération de code est le principal changement de mentalité ; très amical pour les refactorings.
  • Bun : requêtes lisibles et prévisibles ; communauté plus petite mais active ; excellentes fonctionnalités Postgres.
  • sqlc : dépendances minimales en temps d’exécution ; s’intègre bien avec les outils de migration et le CI ; idéal pour les équipes habituées au SQL.

Points forts des fonctionnalités

  • Relations & chargement anticipé : tous gèrent les relations ; GORM (balises + Preload/Joins), Ent (arêtes + .With...()), Bun (Relation(...)), sqlc (vous écrivez les jointures).
  • Migrations : GORM (auto-migrate ; faites attention en production), Ent (SQL généré/diff), Bun (bun/migrate), sqlc (outils externes).
  • Hooks/Extensibilité : GORM (callbacks/plugins), Ent (hooks/middleware + template/codegen), Bun (hooks de requête similaires aux middleware, SQL brut facile), sqlc (composez dans votre couche d’application).
  • JSON/Arrays (Postgres) : Bun et GORM ont des helpers pratiques ; Ent/sqlc gèrent via des types personnalisés ou SQL.

Quand choisir quoi

  • Choisissez GORM si vous souhaitez une commodité maximale, des fonctionnalités riches et une prototypage rapide pour les services CRUD conventionnels.
  • Choisissez Ent si vous valorisez la sécurité au moment de la compilation, des schémas explicites et une maintenance à long terme dans des équipes plus importantes.
  • Choisissez Bun si vous souhaitez une performance et des requêtes SQL explicites avec les commodités ORM là où cela aide.
  • Choisissez sqlc si vous (et votre équipe) préférez le SQL pur avec des liaisons Go typées et aucun surcoût en temps d’exécution.

docker-compose.yml minimal pour PostgreSQL local

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

Packages et bibliothèques ORM en GO

Liens utiles