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 pgxpool pour 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 :

  1. Philosophie SQL-First : Écrivez du SQL quand nécessaire, utilisez le builder quand c’est pratique
  2. Excellente gestion des relations : Chargement eager/lazy, JOINs automatiques, pas de requêtes N+1
  3. Support des transactions : API de transactions de première classe avec gestion d’erreurs appropriée
  4. Performance : Surcharge minimale, fonctionne directement avec le driver pgx
  5. 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.


Crédits & Ressources