Cours NestJS · Module 3: TypeORM et bases de données

Repository Pattern - organiser le trésor

7 min de lecture
Dans cette leçon6

Tu connais déjà les entités et les relations - le trésor a ses rayonnages catalogués et l'on sait ce qui se relie à quoi. Mais qui y accède réellement ? Si chaque service de l'Empire écrivait lui-même du SQL, la même opération "trouver un légionnaire par son nom" existerait à cinq endroits, en cinq versions, avec cinq bugs différents.

Les Romains ont résolu cela autrement : on accède au trésor par un archiviste. Un service ne descend pas à la cave avec une torche - il dit à l'archiviste ce qu'il cherche, et celui-ci sait sur quelle étagère cela se trouve. Dans le code, cet archiviste est un repository : la couche qui sépare la logique métier de la façon dont les données sont stockées.

Trois étapes vers un repository

TypeORM offre un repository gratuitement pour chaque entité - il suffit de le demander. Le chemin est toujours le même, et il vaut la peine de le retenir comme une séquence.

Première étape : l'entité. Tu l'as rencontrée dans les leçons précédentes - une classe avec le décorateur @Entity(), décrivant un rayonnage du trésor. Sans elle, il n'y a rien à archiver.

Deuxième étape : l'enregistrement dans le module. Nous indiquons à NestJS pour quelles entités préparer des archivistes :

1// legionary.module.ts
2@Module({
3  imports: [TypeOrmModule.forFeature([Legionary])],
4  providers: [LegionaryService],
5  controllers: [LegionaryController],
6})
7export class LegionaryModule {}

Note le nom : forFeature, pas forRoot. Cette différence est une source fréquente de confusion, alors séparons-les une fois pour toutes. forRoot s'appelle une seule fois, dans le module racine - il configure la connexion à la base pour toute l'application. forFeature s'appelle dans chaque module séparément, en n'y listant que les entités que ce module utilise vraiment. L'un trace la route vers le trésor, l'autre affecte des archivistes à une cohorte précise.

Troisième étape : l'injection dans le service. Nous recevons l'archiviste enregistré dans le constructeur :

1@Injectable()
2export class LegionaryService {
3  constructor(
4    @InjectRepository(Legionary)
5    private legionaryRepo: Repository<Legionary>,
6  ) {}
7}

Le décorateur @InjectRepository(Legionary) dit à NestJS quel archiviste nous demandons - car une application en compte plusieurs, un par entité. Le type Repository<Legionary> est une notation générique : Repository est le genre général d'archiviste, et <Legionary> le restreint à cette seule entité. Grâce à cela, TypeScript sait que find() renverra des légionnaires et non n'importe quoi - et te suggérera leurs champs pendant que tu écris.

Retiens cet ordre : entité, forFeature dans le module, @InjectRepository dans le service. Sauter l'étape du milieu produit au démarrage une erreur de provider inconnu - et c'est la cause la plus fréquente de ce message.

Lecture - find avec ses options

L'archiviste reçoit ses ordres sous forme d'objet d'options. Sa clé la plus importante est where :

1async findAll(): Promise<Legionary[]> {
2  return this.legionaryRepo.find();
3}
4
5async findCenturions(): Promise<Legionary[]> {
6  return this.legionaryRepo.find({
7    where: { rank: 'Centurion' },
8    relations: ['centurion'],
9    order: { name: 'ASC' },
10  });
11}

find() sans argument rapporte tout. Avec un objet d'options - il restreint. Attention à un détail sur lequel beaucoup trébuchent : les conditions doivent se trouver dans la clé where. Écrire find({ rank: 'Centurion' }) n'est pas un filtre - c'est une option inconnue que TypeORM ignore, et tu obtiendras tous les légionnaires au lieu des seuls centurions. Aucune erreur n'est levée, il est donc facile de passer à côté.

La clé relations charge les entités liées - l'équivalent, côté repository, du leftJoinAndSelect du Query Builder. order trie. Quand il te faut un seul enregistrement, tu appelles findOne({ where: { id } }) - il renvoie l'entité, ou null si rien ne correspond.

Écriture - create et save

Créer un nouveau légionnaire se fait avec une paire de méthodes, et il vaut la peine de comprendre pourquoi deux et non une :

1async create(dto: CreateLegionaryDto): Promise<Legionary> {
2  const legionary = this.legionaryRepo.create(dto);
3  return this.legionaryRepo.save(legionary);
4}

create() ne touche pas à la base. Elle construit seulement un objet entité en mémoire - elle recopie les champs du DTO et lui donne la classe Legionary, pour que décorateurs, hooks et valeurs par défaut fonctionnent. Seul save() l'envoie au trésor et renvoie l'entité enregistrée, portant désormais l'id attribué par la base.

Ce partage a un intérêt pratique : entre create et save, tu peux encore modifier ou vérifier quelque chose. save() a aussi un second visage - appelée sur une entité qui possède déjà un id, elle effectue une mise à jour au lieu d'une insertion. Une méthode, deux comportements, selon que l'enregistrement existe ou non.

Mise à jour - preload

Pour modifier un enregistrement existant, il y a preload, une méthode au nom peu évident :

1async update(id: number, dto: UpdateLegionaryDto): Promise<Legionary> {
2  const legionary = await this.legionaryRepo.preload({ id, ...dto });
3
4  if (!legionary) {
5    throw new NotFoundException(`Le légionnaire ${id} n'existe pas`);
6  }
7
8  return this.legionaryRepo.save(legionary);
9}

preload récupère dans la base l'enregistrement portant l'id indiqué, y applique les champs du DTO et renvoie une entité prête à être enregistrée - mais n'enregistre encore rien. Son avantage est ce que tu n'as pas à faire : les champs absents du DTO conservent leurs valeurs actuelles, une mise à jour partielle n'effacera donc pas le reste de l'enregistrement. Quand aucun enregistrement ne porte cet id, preload renvoie undefined - d'où la vérification avant la sauvegarde. Seul save() rend le changement définitif.

Suppression - remove et delete

Pour finir, deux méthodes de suppression qui font la même chose de manières différentes :

1async remove(id: number): Promise<void> {
2  const legionary = await this.legionaryRepo.findOne({ where: { id } });
3
4  if (!legionary) {
5    throw new NotFoundException(`Le légionnaire ${id} n'existe pas`);
6  }
7
8  await this.legionaryRepo.remove(legionary);
9}

remove() prend une entité - c'est pourquoi nous la récupérons d'abord. Cela coûte une requête supplémentaire, mais en échange tu obtiens la certitude que l'enregistrement existait, et tu déclenches les hooks de suppression. delete(id) prend l'id seul et efface en une requête - plus rapide, mais sans vérification ni hooks. Choisis en conscience : remove là où comptent la justesse et la réaction à un enregistrement manquant, delete là où compte la vitesse.

Récapitulatif

Le trésor a son archiviste et personne ne descend seul à la cave :

  • un repository sépare la logique métier de la façon dont les données sont stockées,
  • le chemin comporte toujours trois étapes : une entité avec @Entity(), TypeOrmModule.forFeature([Entité]) dans le module, @InjectRepository(Entité) dans le constructeur du service,
  • forRoot configure la connexion une fois pour l'application, forFeature affecte des entités à un seul module,
  • find() accepte les options where, relations et order - les conditions hors de la clé where sont ignorées en silence,
  • create() construit une entité en mémoire, save() l'écrit dans la base et sait aussi mettre à jour,
  • preload() fusionne un enregistrement existant avec tes modifications sans enregistrer - il renvoie undefined si l'enregistrement est absent,
  • remove(entité) supprime plus sûrement et avec les hooks, delete(id) plus vite et sans eux.

Dans la prochaine leçon, tu découvriras le Query Builder - l'outil des questions que l'archiviste ne sait pas traiter avec le seul find(). En attendant, retiens ceci : le repository est l'archiviste du trésor - le service dit ce dont il a besoin, pas comment le sortir de l'étagère.

Code de cette leçon : src/repository-pattern.ts
1// Repository Pattern - L'organisation du trésor
2import { Injectable, NotFoundException } from '@nestjs/common';
3import { InjectRepository } from '@nestjs/typeorm';
4import { Repository } from 'typeorm';
5
6console.log("Repository Pattern - le gestionnaire d'accès aux données !");
7
8// ===========================================
9// 1. Service de base avec repository
10// ===========================================
11
12// On suppose l'entité Legionary de la leçon précédente
13
14@Injectable()
15export class LegionaryService {
16  constructor(
17    @InjectRepository('Legionary')
18    private legionaryRepo: Repository<any>,
19  ) {}
20
21  // Récupérer tout
22  async findAll() {
23    return this.legionaryRepo.find();
24  }
25
26  // Récupérer par ID
27  async findById(id: number) {
28    const legionary = await this.legionaryRepo.findOneBy({ id });
29    if (!legionary) {
30      throw new NotFoundException('Legionary non inventus !');
31    }
32    return legionary;
33  }
34
35  // Créer un nouveau
36  async create(data: any) {
37    const legionary = this.legionaryRepo.create(data);
38    return this.legionaryRepo.save(legionary);
39  }
40
41  // Mettre à jour
42  async update(id: number, data: any) {
43    const legionary = await this.findById(id);
44    Object.assign(legionary, data);
45    return this.legionaryRepo.save(legionary);
46  }
47
48  // Supprimer
49  async remove(id: number) {
50    const result = await this.legionaryRepo.delete(id);
51    if (result.affected === 0) {
52      throw new NotFoundException('Legionary non inventus !');
53    }
54  }
55
56  // Trouver par rang
57  async findByRank(rank: string) {
58    return this.legionaryRepo.find({ where: { rank } });
59  }
60
61  // Compter les légionnaires d'une légion
62  async countByLegion(legionName: string) {
63    return this.legionaryRepo.count({
64      where: { legion: legionName },
65    });
66  }
67}
68
69// ===========================================
70// 2. Service CRUD avec garnison
71// ===========================================
72
73@Injectable()
74export class GarrisonService {
75  constructor(
76    @InjectRepository('Garrison')
77    private garrisonRepo: Repository<any>,
78  ) {}
79
80  async findAll() {
81    return this.garrisonRepo.find({
82      order: { soldiers: 'DESC' },
83    });
84  }
85
86  async create(data: any) {
87    return this.garrisonRepo.save(data);
88  }
89
90  async update(id: number, data: any) {
91    await this.garrisonRepo.update(id, data);
92    return this.garrisonRepo.findOneBy({ id });
93  }
94
95  async remove(id: number) {
96    await this.garrisonRepo.delete(id);
97  }
98}
99
100// ===========================================
101// Enregistrement dans un modulee
102// ===========================================
103
104// @Module({
105//   imports: [TypeOrmModule.forFeature([Legionary, Garrison])],
106//   providers: [LegionaryService, GarrisonService],
107//   exports: [LegionaryService],
108// })
109// export class LegionaryModule {}
110
111console.log("\n=== RÉSUMÉ DU REPOSITORY ===");
112console.log("@InjectRepository(Entity) - injecte le repository");
113console.log("repo.find() - récupère tous les enregistrements");
114console.log("repo.findOneBy() - récupère un enregistrement");
115console.log("repo.save() - enregistre (insert ou update)");
116console.log("repo.delete() - supprime un enregistrement");
117console.log("repo.count() - compte les enregistrements");
118

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Comment enregistre-t-on un repository d'entité dans un module NestJS ?

  2. 2. Comment trouve-t-on tous les légionnaires de rang 'Centurion' à l'aide d'un repository TypeORM ?

Tâches pratiques dans le jeu

  • Tri vertical

    Classe les étapes de configuration d'un repository TypeORM dans un module NestJS :

  • Éditeur de code

    Écris un LegionaryService avec @InjectRepository(Legionary) et les méthodes findAll(), findOne(id), create(dto) et remove(id)

  • Tri horizontal

    Classe les éléments de l'injection d'un repository dans le constructeur du service :

  • Clique dans l'ordre

    Classe les éléments de l'enregistrement d'une nouvelle entité via un repository :

  • Éditeur de code

    Écris un service avec les méthodes : find() avec where et relations, create() avec save, update() avec preload et save, delete() avec remove