Skip to content

Le pattern Repository fr FR

rocambille edited this page Apr 28, 2026 · 5 revisions

Résumé : Pour accéder à la base de données, StartER adopte le pattern Repository. Ce modèle encapsule les requêtes SQL dans des classes dédiées et fournit une interface claire pour effectuer les opérations CRUD.

Séparer l'accès aux données

Cela permet de séparer le code SQL du reste de l'application et de faciliter la maintenance, les tests et l'évolution du modèle. L'ensemble des communications avec une table spécifique doit passer par son Repository.

Implémentation du Repository

Un exemple d'implémentation est proposé dans src/express/modules/item/itemRepository.ts. StartER tire parti de l'API synchrone native de SQLite, ce qui simplifie grandement le code : pas de async, pas de await.

import database from "../../../database";

class ItemRepository {
  // Le C de CRUD - Opération Create
  create(item: Omit<Item, "id">): number | bigint {
    const query = database.prepare(
      "insert into item (title, user_id) values (?, ?)",
    );

    const result = query.run(item.title, item.user_id);

    return result.lastInsertRowid;
  }

  // Le R de CRUD - Opération Read
  find(byId: number): Item | null {
    const query = database.prepare(
      "select id, title, user_id from item where id = ? and deleted_at is null",
    );

    const row = query.get(byId);

    if (row == null) {
      return null;
    }

    const { id, title, user_id } = row;

    return { id: Number(id), title: String(title), user_id: Number(user_id) };
  }

  // ...
}

export default new ItemRepository();

Ainsi, chaque module Express peut disposer de son propre repository, ce qui garantit une organisation claire et extensible du code.

Un mot sur TypeScript et la robustesse

SQLite ne retourne pas des types TypeScript stricts, mais des valeurs SQL de base (string | number | bigint | null, etc.). Au lieu d'utiliser des assertions de type TypeScript (comme as Item qui masquerait d'éventuelles erreurs au moment de la compilation sans protéger l'exécution), StartER privilégie le "casting" explicite au runtime lors de la reconstruction de l'objet :

return { id: Number(id), title: String(title), user_id: Number(user_id) };

Cette approche garantit que l'objet retourné correspond exactement au contrat attendu par le reste de l'application. Si la base de données retourne une donnée inattendue, le casting explicite prévient des comportements imprévisibles (bugs silencieux) en imposant le bon type.

Utilisation dans Express (Actions)

Une action Express gère le cycle requête/réponse et fait appel au repository pour la donnée. Même si le repository est synchrone, l'action Express reste généralement asynchrone (async) pour pouvoir interagir avec d'autres modules (lecture du req.body, requêtes externes, etc.).

import itemRepository from "./itemRepository";

const browse = async (req, res) => {
  const items = itemRepository.findAll(10, 0); // Appel direct et synchrone
  res.json(items);
};

export default { browse };

Toutes les actions des items et le repository complet (lié à la base de données) sont disponibles dans les fichiers suivants :

  • src/express/modules/item/itemActions.ts
  • src/express/modules/item/itemRepository.ts

Aller plus loin : pagination

Le repository propose une pagination intégrée via findAll(limit, offset) :

  findAll(limit: number, offset: number): Item[] {
    const query = database.prepare(
      "select id, title, user_id from item where deleted_at is null limit ? offset ?",
    );
    
    const rows = query.all(limit, offset);

    // On utilise map() avec casting explicite pour chaque ligne
    return rows.map<Item>(({ id, title, user_id }) => ({
      id: Number(id),
      title: String(title),
      user_id: Number(user_id),
    }));
  }

L'action browse dans itemActions.ts utilise un offset calculé depuis le paramètre de requête ?start= :

const offset = Number(req.query.start ?? "0");

const items = itemRepository.findAll(10, offset);

Tip

Ce mécanisme de pagination basique est suffisant pour démarrer. Pour des cas plus avancés, vous pouvez ajouter un compteur total, un tri configurable, ou une pagination par curseur.

Aller plus loin : suppression douce (soft delete)

StartER utilise une stratégie de suppression douce (soft delete) : les enregistrements ne sont pas physiquement supprimés de la base, mais marqués avec un timestamp deleted_at.

Le repository propose trois méthodes complémentaires :

Méthode Comportement
softDelete Marque l'enregistrement comme supprimé (deleted_at = datetime('now'))
softUndelete Restaure un enregistrement supprimé (deleted_at = null)
hardDelete Supprime définitivement l'enregistrement de la base
  softDelete(id: number): number | bigint {
    const query = database.prepare(
      "update item set deleted_at = datetime('now') where id = ?",
    );
    const result = query.run(id);

    return result.changes; // Retourne le nombre de lignes affectées
  }

Les requêtes de lecture (findAll, find) filtrent automatiquement les enregistrements supprimés grâce à la clause where deleted_at is null.

Important

Par défaut, l'action destroy dans itemActions.ts utilise softDelete. Pour supprimer définitivement un enregistrement, utilisez hardDelete en connaissance de cause.

Bonnes pratiques et cas d'usage

  • Privilégier le casting explicite au runtime : évitez les assertions TypeScript aveugles (as Type). Reconstruisez vos objets avec Number(), String(), etc., pour garantir la sécurité et l'intégrité de vos données en cours d'exécution.
  • Pagination par défaut : pensez à limiter vos requêtes SQL pour éviter de surcharger l'application sur de grandes tables.
  • Privilégier le Soft Delete : garder un historique avec deleted_at sécurise la donnée et simplifie les restaurations en cas d'erreur.

Voir aussi

Clone this wiki locally