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

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/migratesé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
JoinsouPreloadsé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,INSERTmulti-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