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

Un aperçu pratique et riche en code des ORM en Go

Sommaire

Les ORM les plus populaires pour GO sont GORM, Ent, Bun et sqlc. Voici une petite comparaison de ces outils avec des exemples d’opérations CRUD en pur GO.

golang + postgresql

TL;DR (Version abrégée)

  • GORM : riche en fonctionnalités et pratique ; le plus simple pour « livrer rapidement », mais avec un surcoût en temps d’exécution plus élevé.
  • Ent : schéma en code avec des API générées et typées de manière sûre ; excellent pour les grandes bases de code et les refactors.
  • Bun : léger, axé SQL, constructeur de requêtes/ORM ; rapide avec d’excellentes fonctionnalités Postgres, explicite par conception.
  • sqlc (pas un ORM à proprement parler, mais tout de même) : écrivez du SQL, obtenez du Go typé de manière sûre ; meilleure performance brute et contrôle, sans magie en temps d’exécution.

Critères de sélection et comparaison rapide

Mes critères sont les suivants :

  • Performance : latence/débit, surcoût évitable, opérations par lot.
  • DX (Expérience développeur) : courbe d’apprentissage, sécurité des types, débogabilité, friction de génération de code.
  • Écosystème : documentation, exemples, activité, intégrations (migrations, traçabilité).
  • Ensemble de fonctionnalités : relations, chargement anticipé (eager loading), migrations, hooks, accès direct au SQL brut.
Outil Paradigme Sécurité des types Relations Migrations Ergonomie du SQL brut Cas d’usage typique
GORM ORM de type Active Record Moyenne (temps d’exécution) Oui (tags, Preload/Joins) Auto-migration (optionnel) db.Raw(...) Livraison rapide, fonctionnalités riches, applications CRUD conventionnelles
Ent Schéma → génération de code → API fluide Élevée (temps de compilation) De premier ordre (edges) SQL généré (étape séparée) entsql, SQL personnalisé Grandes bases de code, équipes avec beaucoup de refactors, typage strict
Bun Constructeur de requêtes/ORM axé SQL Moyenne–Élevée Explicite (Relation) Package de migration séparé Naturel (constructeur + brut) Services sensibles à la performance, fonctionnalités Postgres
sqlc SQL → fonctions générées (pas un ORM) Élevée (temps de compilation) Via des jointures SQL Outil externe (ex. golang-migrate) C’est du SQL Contrôle maximal et vitesse ; équipes familières avec les BDD

CRUD par exemples

Mise en place (PostgreSQL)

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

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

Imports (communs à tous les ORM)

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

import (
  "context"
  "os"
)

Nous allons modéliser une simple table 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-migration (optionnel ; attention en production)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }

CRUD

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

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

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

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

  return nil
}

Notes

  • Relations via les struct tags + Preload/Joins.
  • Assistant pour les transactions : 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ération 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éation
  u, err := client.User.Create().
    SetName("Alice").
    SetEmail("alice@example.com").
    Save(ctx)
  if err != nil { return err }

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

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

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

  return nil
}

Notes

  • Typage fort de bout en bout ; edges pour les relations.
  • Migrations générées ou utilisation de votre outil de migration préféré.

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

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

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

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

  return nil
}

Notes

  • Jointures/chargement anticipé explicites avec .Relation("...").
  • Package bun/migrate séparé 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 de manière sûre.

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"

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ération

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éation
  u, err := q.CreateUser(ctx, db.CreateUserParams{
    Name: "Alice", Email: "alice@example.com",
  })
  if err != nil { return err }

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

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

  // Suppression
  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 de SQL ou combinez avec un petit constructeur.

Notes sur la performance

  • GORM : pratique, mais ajoute un surcoût de réflexion/abstraction. Convient pour un CRUD typique ; attention aux requêtes N+1 (préférez Joins ou Preload sélectif).
  • Ent : le code généré évite la réflexion ; bon pour les schémas complexes. Souvent plus rapide que les ORM lourds, basés sur la magie en temps d’exécution.
  • Bun : mince sur database/sql ; rapide, explicite, excellent pour les opérations par lot et les grands ensembles de résultats.
  • sqlc : essentiellement des performances du SQL brut avec une sécurité en temps de compilation.

Conseils généraux

  • Utilisez pgx pour le pilote (v5) et context partout.
  • Préférez le groupement par lot (COPY, INSERT multi-lignes) pour un haut débit.
  • Profilez le SQL : EXPLAIN ANALYZE, index, index couvrants, éviter les allers-retours inutiles.
  • Réutilisez les connexions ; ajustez la taille du pool en fonction de la charge de travail.

Expérience développeur et Écosystème

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

Points forts fonctionnels

  • Relations & chargement anticipé : tous gèrent les relations ; GORM (tags + Preload/Joins), Ent (edges + .With...()), Bun (Relation(...)), sqlc (vous écrivez les jointures).
  • Migrations : GORM (auto-migration ; attention en prod), Ent (SQL généré/différence), Bun (bun/migrate), sqlc (outils externes).
  • Hooks/Extensibilité : GORM (callbacks/plugins), Ent (hooks/middleware + modèle/génération de code), Bun (hooks de requête de type middleware, SQL brut facile), sqlc (composition dans votre couche applicative).
  • JSON/Arrays (Postgres) : Bun et GORM ont de bons assistants ; Ent/sqlc le gèrent via des types personnalisés ou du SQL.

Une note transversale pour les quatre : Go 1.27 appuie désormais encoding/json avec le moteur v2, ce qui préserve le comportement mais modifie certains textes d’erreur et, sous l’API explicite v2, la sortie des tranches nil et des clés de map. Si vos modèles font aller-retour en JSON, vérifiez ce que dit le guide de migration Go 1.27 concernant les fixtures de test dorés avant de migrer.

Quand choisir quoi

  • Choisissez GORM si vous voulez un maximum de commodité, des fonctionnalités riches et un prototypage rapide pour des services CRUD conventionnels.
  • Choisissez Ent si vous valuez la sécurité en temps de compilation, des schémas explicites et une maintenabilité à long terme dans les grandes équipes.
  • Choisissez Bun si vous voulez de la performance et des requêtes de forme SQL explicites avec le confort d’un ORM là où c’est utile.
  • Choisissez sqlc si vous (et votre équipe) préférez le SQL pur avec des bindings Go typés de manière sûre et zéro surcoût en temps d’exécution. sqlc est également un choix naturel pour le côté modèle de lecture d’une architecture CQRS en Go où les requêtes sont façonnées pour les appelants plutôt que pour les entités de domaine et le SQL explicite vous donne un contrôle total sur la projection.

Si vous êtes toujours en train d’arbitrer ce choix d’ORM par rapport au style d’intégration et aux limites de service, ce survol de l’architecture applicative aide à situer la décision dans un contexte de production plus large.


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

Paquets ORM et bibliothèques en GO

Autres liens utiles

S'abonner

Recevez de nouveaux articles sur les systèmes, l'infrastructure et l'ingénierie IA.