Bun, entre une approche SQL et ORM pour Go.
Pourquoi Bun ORM ?
Bun est un ORM pour Go qui trouve l’équilibre parfait entre la puissance du SQL brut et la commodité d’un ORM. Contrairement aux ORMs lourds qui abstraient le SQL, Bun vous permet d’utiliser la puissance des deux offrant la sûreté des types, d’excellentes performances et la possibilité d’écrire du SQL quand vous en avez besoin.
Dans cet article, je vais vous présenter une API de tâches pratiques construite avec Bun, démontrant comment il gère les relations, les transactions et la construction de requêtes de manière simple et maintenable.
Vue d’ensemble du projet
Le projet est une API REST de gestion de tâches avec la stack suivante :
- Bun ORM avec le driver pgx/v5 pour PostgreSQL
- Gin pour le routage HTTP
- Architecture choisie : Handlers → Usecases → Repositories
- Pool de connexions avec
pgxpoolpour une utilisation optimale des ressources
Définir des modèles avec Bun
Bun utilise des tags de struct pour mapper les structs Go aux tables de base de données. Voici comment définir une tâche avec ses éléments associés :
type Task struct {
bun.BaseModel `bun:"table:tasks,alias:t"`
ID int64 `bun:"id,pk,autoincrement"`
Title string `bun:"title,notnull"`
Description string `bun:"description"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
Items []*TaskItem `bun:"rel:has-many,join:id=task_id"`
}
type TaskItem struct {
bun.BaseModel `bun:"table:task_items,alias:ti"`
ID int64 `bun:"id,pk,autoincrement"`
TaskID int64 `bun:"task_id,notnull"`
Title string `bun:"title,notnull"`
Completed bool `bun:"completed,notnull,default:false"`
Task *Task `bun:"rel:belongs-to,join:task_id=id"`
}
Le tag rel:has-many crée une relation one-to-many. Bun gère automatiquement la logique des JOINs quand vous en avez besoin.
Couche Repository : Requêtes Type-Safe
Le query builder de Bun offre la sûreté des types tout en restant proche du SQL :
func (r *taskRepository) Create(ctx context.Context, task *models.Task) error {
return r.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Insertion de la tâche
if _, err := tx.NewInsert().Model(task).Exec(ctx); err != nil {
return err
}
// Insertion des éléments associés
if len(task.Items) > 0 {
for _, item := range task.Items {
item.TaskID = task.ID
}
if _, err := tx.NewInsert().Model(&task.Items).Exec(ctx); err != nil {
return err
}
}
return nil
})
}
Remarquez comment RunInTx gère proprement les transactions—rollback en cas d’erreur, commit en cas de succès. Pas besoin de gestion manuelle des transactions.
Chargement des Relations
Récupérer une tâche avec ses éléments est simple avec la méthode Relation() de Bun :
func (r *taskRepository) GetByID(ctx context.Context, taskID int64) (*models.Task, error) {
task := new(models.Task)
err := r.db.NewSelect().
Model(task).
Relation("Items").
Where("t.id = ?", taskID).
Scan(ctx)
if err != nil {
return nil, err
}
return task, nil
}
Bun génère automatiquement la requête JOIN et mappe les résultats vers la struct imbriquée.
Pool de Connexions avec pgxpool
Pour la production, le projet utilise pgxpool pour un contrôle fin du pool de connexions :
poolConfig, _ := pgxpool.ParseConfig(dsn)
poolConfig.MinConns = 3 // Maintenir un minimum de connexions
poolConfig.MaxConns = 5 // Limiter le nombre maximum de connexions
pool, _ := pgxpool.NewWithConfig(ctx, poolConfig)
// Conversion vers database/sql pour la compatibilité Bun
sqldb := stdlib.OpenDBFromPool(pool)
bunDB := bun.NewDB(sqldb, pgdialect.New())
Cette approche respecte le modèle de ressources PostgreSQL (chaque connexion = un processus backend) tout en maintenant un pool actif pour les performances.
Pourquoi j’ai choisi Bun
Dans un prochain article, nous regarderons une approche SQL first avec PGX natif directement, qui en Golang est très pratique. Ici j’ai cherché à présenter une approche alliant un compromis entre les deux mondes que nous permet Bun :
- ORM,
- SQL First.
Pour l’utiliser chez mon client, au quotidien, voici ce qui ressort de Bun :
- Philosophie SQL-First : Écrivez du SQL quand nécessaire, utilisez le builder quand c’est pratique
- Excellente gestion des relations : Chargement eager/lazy, JOINs automatiques, pas de requêtes N+1
- Support des transactions : API de transactions de première classe avec gestion d’erreurs appropriée
- Performance : Surcharge minimale, fonctionne directement avec le driver pgx
- Sûreté des types : Vérifications à la compilation sans sacrifier la flexibilité
Conclusion
Bun ORM prouve que vous n’avez pas à choisir entre contrôle et commodité. Il fournit juste assez d’abstraction pour éliminer le code répétitif tout en vous gardant proche du SQL. Parfait pour les développeurs qui veulent la productivité d’un ORM sans renoncer à la puissance du SQL.