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

Seeders et Fixtures - l'équipement initial de la cohorte

7 min de lecture
Dans cette leçon7

Le consul Caesar.js en a assez d'un certain rituel. Chaque fois qu'un nouveau développeur rejoint le projet du trésor, ou qu'une base fraîche est installée pour les tests, quelqu'un saisit à la main les mêmes données : quelques centurions, deux ou trois légionnaires, les compétences de base. Une base fraîche est une castra vide - et une cohorte sans équipement ne partira pas en campagne. Il est temps d'engager un quartier-maître qui le fera pour nous : un seeder.

Seeder ou fixtures - le code et les provisions

Ces deux notions adorent se mélanger, alors séparons-les tout de suite. Un seeder, c'est du code - une classe qui, une fois lancée, remplit la base de données initiales. Les fixtures, ce sont les données elles-mêmes - des ensembles statiques rangés dans des fichiers (JSON, YAML), qui attendent qu'on les charge. En termes romains : les fixtures sont la liste des provisions sur le parchemin, le seeder est le quartier-maître qui charge les chariots selon cette liste. Un seeder peut avoir ses données écrites directement dans le code, ou les lire justement depuis un fichier de fixtures.

Ton premier seeder

Nous utiliserons le paquet typeorm-extension, qui ajoute la gestion des seeds à TypeORM. Un seeder est une classe implémentant l'interface Seeder avec une seule méthode run - elle reçoit la connexion à la base (dataSource), d'où nous tirons les repositories que nous connaissons déjà :

1// seeds/centurion.seeder.ts
2import { DataSource } from 'typeorm';
3import { Seeder } from 'typeorm-extension';
4import { Centurion } from '../src/centurion/centurion.entity';
5
6export default class CenturionSeeder implements Seeder {
7  public async run(dataSource: DataSource): Promise<void> {
8    const centurionRepository = dataSource.getRepository(Centurion);
9
10    await centurionRepository.save([
11      { name: 'Maximus Decimus', cohortName: 'Legio X Equestris', yearsOfExperience: 15 },
12      { name: 'Livia Drusilla', cohortName: 'Legio III Gallica', yearsOfExperience: 8 },
13    ]);
14  }
15}

Rien au-delà de ce que tu sais déjà : nous prenons un repository et appelons save avec un tableau d'objets. Toute la nouveauté tient dans l'emballage : une classe avec une méthode run peut être exécutée par une seule commande - sur une base fraîche, chez chaque développeur, dans chaque environnement - et elle chargera à chaque fois exactement le même équipement.

Nettoyage et idempotence - le seeder lancé deux fois

Que se passe-t-il si quelqu'un relance le seeder ? Avec le code ci-dessus - une deuxième fournée des mêmes centurions et une base pleine de doublons. Le quartier-maître doit y être préparé, et il existe deux stratégies :

1public async run(dataSource: DataSource): Promise<void> {
2  const centurionRepository = dataSource.getRepository(Centurion);
3
4  // Stratégie 1 : vider la table avant le seeding
5  await centurionRepository.clear();
6
7  // Stratégie 2 : si les données existent déjà, ne rien faire
8  const existing = await centurionRepository.count();
9  if (existing > 0) {
10    return;
11  }
12
13  await centurionRepository.save([ /* ... */ ]);
14}

En pratique, tu choisis l'une des deux. clear() vide la table et seede à partir de zéro - le résultat est toujours prévisible, c'est donc un bon choix pour les bases de développement et de test. La vérification par count() laisse les données existantes tranquilles - plus sûr là où la base peut déjà contenir quelque chose de précieux. Les deux versions partagent la propriété dont il s'agit ici : le seeder peut être lancé autant de fois qu'on veut sans que la base devienne un tas de doublons. Cette résistance aux exécutions répétées s'appelle l'idempotence - et je te recommande de la traiter comme une exigence de fer pour chaque seeder.

L'ordre compte - les relations entre tables

Dans notre système, un légionnaire appartient à un centurion. Cela signifie que nous ne pouvons pas semer des légionnaires dans une base vide - leurs enregistrements pointent vers des centurions qui doivent exister avant. Le quartier-maître charge les chariots dans un ordre fixé :

1// 1. D'abord les entités indépendantes
2const centurions = await centurionRepository.save([
3  { name: 'Maximus Decimus', cohortName: 'Legio X Equestris' },
4]);
5
6// 2. Puis les entités qui s'y réfèrent
7await legionaryRepository.save([
8  { name: 'Marcus Antonius', rank: 'Speculator', centurion: centurions[0] },
9  { name: 'Gaius Julius', rank: 'Optio', centurion: centurions[0] },
10]);

Remarque que save renvoie les objets enregistrés - déjà munis des identifiants attribués par la base. C'est ce qui nous permet, à la deuxième étape, de passer centurions[0] comme propriétaire de la relation. La règle générale dit : d'abord les tables vers lesquelles les autres pointent (compétences, rangs, centurions), puis les tables dépendantes (légionnaires), et enfin les données qui lient les unes aux autres (tributs avec propriétaire attribué). Le nettoyage s'effectue dans l'ordre inverse - les dépendantes d'abord, les indépendantes en dernier.

La fabrique de données - cent légionnaires pour les tests

Les données initiales, nous les écrivons à la main, car il y en a peu. Mais pour des tests de performance ou un travail sur la pagination, il te faut des centaines d'enregistrements - et personne ne va les inventer. C'est le rôle de la fabrique : une recette pour un objet aléatoire, que la bibliothèque @faker-js/faker remplit de données inventées :

1// factories/legionary.factory.ts
2import { setSeederFactory } from 'typeorm-extension';
3import { Legionary } from '../src/legionariusze/legionariusze.entity';
4
5export default setSeederFactory(Legionary, (faker) => {
6  const legionary = new Legionary();
7  legionary.name = faker.person.fullName();
8  legionary.rank = faker.helpers.arrayElement(['Legionary', 'Speculator', 'Optio']);
9  legionary.tributeCount = faker.number.int({ min: 0, max: 50 });
10  legionary.isActive = faker.datatype.boolean();
11  return legionary;
12});

La fabrique décrit à quoi ressemble un légionnaire aléatoire : un nom qui sonne vrai, un rang tiré d'une liste, un compteur de tributs dans un intervalle donné. La multiplication tient en une ligne dans le seeder :

1const legionaryFactory = factoryManager.get(Legionary);
2await legionaryFactory.saveMany(100);

Et la castra est pleine : cent légionnaires différents, chacun avec ses données, zéro travail manuel. Note le partage des rôles - la fabrique définit la forme d'un seul enregistrement, tandis que le seeder décide combien en naissent et quand.

Lancement - on relie le tout

Pour finir, nous disons à TypeORM où chercher les seeds et les fabriques. Nous étendons la configuration de la source de données avec deux champs :

1// data-source.ts
2const options: DataSourceOptions & SeederOptions = {
3  type: 'postgres',
4  database: 'legionariusze_cohort',
5  entities: ['src/**/*.entity{.ts,.js}'],
6  seeds: ['src/seeds/*{.ts,.js}'],
7  factories: ['src/factories/*{.ts,.js}'],
8};

Le champ seeds pointe vers les classes de seeders, factories vers les recettes des fabriques. Désormais, une seule commande dans le terminal, npm run seed:run, parcourt tous les seeds et équipe la base. Une castra fraîche devient un camp prêt en quelques secondes.

Récapitulatif

Ta cohorte ne partira plus jamais sans provisions :

  • un seeder est du code qui charge les données initiales, les fixtures sont des données statiques dans des fichiers - la liste des provisions et le quartier-maître qui l'exécute,
  • un seeder doit être idempotent : vide la table avec clear(), ou saute le seeding quand count() détecte des données existantes,
  • avec les relations, l'ordre est sacré : d'abord les entités indépendantes, puis les dépendantes - et le nettoyage en sens inverse,
  • une fabrique avec faker décrit un enregistrement aléatoire, et saveMany(100) le multiplie pour les tests,
  • les champs seeds et factories de la configuration relient le tout en une commande seed:run.

Dans la prochaine leçon, nous nous attaquerons à la validation des données - puisque le trésor sait maintenant s'équiper tout seul, il est temps de veiller à ce que rien d'abîmé n'y entre. En attendant, retiens ceci : le seeder est le quartier-maître de la cohorte - grâce à lui, chaque base fraîche reçoit le même équipement complet en une seule commande.

Code de cette leçon : src/seeders.ts
1// Seeders et Fixtures - La cargaison initiale du navire
2import { DataSource } from 'typeorm';
3
4console.log("Seeders - le chargement automatique des données initiales !");
5
6// ===========================================
7// 1. Seeder de base
8// ===========================================
9
10export async function seedLegionaries(dataSource: DataSource): Promise<void> {
11  const legionaries = [
12    { name: 'Marcus Aurelius', rank: 'Legatus', experience: 15, legion: 'Legio X Gemina' },
13    { name: 'Gaius Julius', rank: 'Centurio', experience: 10, legion: 'Legio III Augusta' },
14    { name: 'Lucius Scipio', rank: 'Optio', experience: 7, legion: 'Legio IX Hispana' },
15    { name: 'Titus Flavius', rank: 'Miles', experience: 3, legion: 'Legio XII Fulminata' },
16    { name: 'Publius Decimus', rank: 'Miles', experience: 1, legion: 'Legio V Macedonica' },
17  ];
18
19  await dataSource
20    .createQueryBuilder()
21    .insert()
22    .into('legionaries')
23    .values(legionaries)
24    .execute();
25
26  console.log('Chargement de ' + legionaries.length + ' légionnaires !');
27}
28
29// ===========================================
30// 2. Seeder de provinces
31// ===========================================
32
33export async function seedProvinces(dataSource: DataSource): Promise<void> {
34  const provinces = [
35    { name: 'Gallia', governor: 'Vercingetorix', population: 120000, tribute: 50000 },
36    { name: 'Aegyptus', governor: 'Cleopatra', population: 300000, tribute: 80000 },
37    { name: 'Britannia', governor: 'Boudicca', population: 80000, tribute: 20000 },
38    { name: 'Hispania', governor: 'Scipio', population: 150000, tribute: 45000 },
39    { name: 'Africa', governor: 'Jugurtha', population: 200000, tribute: 60000 },
40  ];
41
42  await dataSource
43    .createQueryBuilder()
44    .insert()
45    .into('provinciae')
46    .values(provinces)
47    .execute();
48
49  console.log('Chargement de ' + provinces.length + ' provinces !');
50}
51
52// ===========================================
53// 3. Fabrique de données de test (Factory)
54// ===========================================
55
56const ROMAN_NAMES = ['Marcus', 'Gaius', 'Lucius', 'Titus', 'Publius'];
57const SURNAMES = ['Aurelius', 'Julius', 'Scipio', 'Flavius', 'Decimus'];
58const RANKS = ['Miles', 'Optio', 'Centurio', 'Tribunus', 'Legatus'];
59
60function createMockLegionary(overrides?: any) {
61  const randomFrom = (arr: string[]) => arr[Math.floor(Math.random() * arr.length)];
62
63  return {
64    id: Math.floor(Math.random() * 1000),
65    name: randomFrom(ROMAN_NAMES) + ' ' + randomFrom(SURNAMES),
66    rank: randomFrom(RANKS),
67    experience: Math.floor(Math.random() * 20),
68    legion: 'Legio ' + randomFrom(['X', 'III', 'IX', 'V', 'XII']),
69    ...overrides,
70  };
71}
72
73function createMockLegionaries(count: number) {
74  return Array.from({ length: count }, (_, i) =>
75    createMockLegionary({ id: i + 1 }),
76  );
77}
78
79// ===========================================
80// 4. Runner principal
81// ===========================================
82
83// async function runSeeds() {
84//   const dataSource = new DataSource({ ... });
85//   await dataSource.initialize();
86//   await seedLegionaries(dataSource);
87//   await seedProvinces(dataSource);
88//   await dataSource.destroy();
89//   console.log('Seeding terminé !');
90// }
91
92console.log("\n=== RÉSUMÉ DES SEEDERS ===");
93console.log("Seeder - fonction chargeant les données initiales");
94console.log("Factory - génère des données de test aléatoires");
95console.log("Fixtures - données statiques dans des fichiers");
96console.log("createQueryBuilder().insert().into().values() - insertion");
97console.log("Lance les seeds après les migrations !");
98

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. À quoi servent les seeders de base de données ?

  2. 2. Quelle est la principale différence entre les fixtures et les seeders ?

Tâches pratiques dans le jeu

  • Tri vertical

    Classe les étapes du processus de seeding de base de données :

  • Éditeur de code

    Crée une fonction seed() qui se connecte à la base de données, vide la table des légionnaires et insère 5 nouveaux légionnaires

  • Clique dans l'ordre

    Classe les tables dans le bon ordre de seeding (les tables sans dépendances en premier) :

  • Tri horizontal

    Classe les éléments du vidage d'une table via un repository dans le bon ordre :

  • Éditeur de code

    Écris une fonction createMockLegionary(overrides?) qui retourne un objet légionnaire dont les valeurs par défaut sont remplacées par overrides