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

Un regard pratique et axé sur le code sur les ORM en Go

Sommaire

Les ORMs pour GO les plus connus sont GORM, Ent, Bun et sqlc. Voici une petite comparaison de ceux-ci avec des exemples d’opérations CRUD en GO pur.

golang + postgresql

En résumé

  • GORM : complet et pratique ; le plus simple pour « sortir » rapidement, mais avec un surcoût d’exécution plus important.
  • Ent : schéma en code avec des APIs générées et typées ; excellent pour les grandes bases de code et les refactorings.
  • Bun : constructeur de requêtes SQL/ORM léger ; rapide avec d’excellentes fonctionnalités Postgres, explicite par conception.
  • sqlc (pas un ORM à proprement parler, mais néanmoins) : écrivez du SQL, obtenez du Go typé ; meilleures performances et contrôle brut, sans magie à l’exécution.

Critères de sélection et comparaison rapide

Mes critères sont :

  • Performance : latence/débit, surcoûts évitables, opérations par lots.
  • Expérience développeur (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çabilité).
  • Ensemble de fonctionnalités : relations, chargement eager (préchargement), migrations, hooks, échappatoires vers le SQL brut.
Outil Paradigme Sécurité des types Relations Migrations Ergonomie du SQL brut Cas d’utilisation typique
GORM ORM de style Active Record Moyenne (à l’exécution) Oui (balises, Preload/Joins) Auto-migrate (optionnel) db.Raw(...) Livraison rapide, fonctionnalités riches, applications CRUD conventionnelles
Ent Schéma → codegen → API fluide Élevée (à la compilation) De première classe (arêtes) SQL généré (étape séparée) entsql, SQL personnalisé Grandes bases de code, équipes avec beaucoup de refactoring, typage strict
Bun Constructeur de requêtes/ORM orienté SQL Moyenne–Élevée Explicite (Relation) Package de migration séparé Naturelle (constructeur + brut) Services sensibles aux performances, fonctionnalités Postgres
sqlc SQL → fonctions générées (pas un ORM) Élevée (à la compilation) Via des jointures SQL Outil externe (ex., golang-migrate) C’est du SQL Contrôle et vitesse maximum ; équipes amies des DBA

CRUD par exemple

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 ORMs)

Au début de chaque fichier contenant des exemples de code go, ajoutez :

import (
  "context"
  "os"
)

Nous allons modéliser une table users simple :

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 ; soyez prudent 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.
  • Assistant 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 le 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 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é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

  • Joins/chargement eager 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.

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 constructeur.

Notes sur les performances

  • GORM : pratique mais ajoute un surcoût de réflexion/abstraction. Correct pour le CRUD typique ; attention aux requêtes N+1 (privilégiez 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 ORMs lourds avec magie à l’exécution.
  • Bun : mince couche sur database/sql ; rapide, explicite, excellent pour les opérations par lots et les grands ensembles de résultats.
  • sqlc : essentiellement des performances SQL brutes avec sécurité à la compilation.

Conseils généraux

  • Utilisez pgx comme pilote (v5) et context partout.
  • Privilégiez le batching (COPY, INSERT multi-lignes) pour un haut débit.
  • Profilez le 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 développeur et écosystème

  • GORM : plus grande communauté, nombreux exemples/plugins ; courbe d’apprentissage plus raide pour les patterns avancés.
  • Ent : excellente documentation ; l’étape de codegen est le principal changement de modèle mental ; super friendly au refactoring.
  • 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 CI ; superbe pour les équipes à l’aise avec le SQL.

Points forts fonctionnels

  • Relations & chargement eager : tous gèrent les relations ; GORM (balises + Preload/Joins), Ent (arêtes + .With...()), Bun (Relation(...)), sqlc (vous écrivez les jointures).
  • Migrations : GORM (auto-migrate ; prudence en prod), 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 style middleware, SQL brut facile), sqlc (composition dans votre couche applicative).
  • JSON/Arrays (Postgres) : Bun et GORM ont de beaux assistants ; Ent/sqlc gèrent via des types personnalisés ou du SQL.

Quand choisir quoi

  • Choisissez GORM si vous voulez un confort maximum, des fonctionnalités riches et un prototypage rapide pour des services CRUD conventionnels.
  • Choisissez Ent si vous valorisez la sécurité à la compilation, des schémas explicites et la maintenabilité à long terme dans de grandes équipes.
  • Choisissez Bun si vous voulez des performances et des requêtes SQL explicites avec le confort de l’ORM là où cela aide.
  • Choisissez sqlc si vous (et votre équipe) préférez le SQL pur avec des bindings Go typés et zéro surcoût 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 du domaine et le SQL explicite vous donne un contrôle total sur la projection.

Si vous équilibrez encore ce choix d’ORM entre style d’intégration et frontières de services, cet aperçu de l’architecture applicative aide à placer 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

Packages et libs ORM en GO

Autres liens utiles

S'abonner

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