Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Seeders i Fixtures - początkowe wyposażenie kohorty

Konsul Caesar.js ma dość pewnego rytuału. Za każdym razem, gdy nowy programista dołącza do prac nad systemem skarbca albo gdy stawiamy świeżą bazę do testów, ktoś ręcznie wklepuje te same dane: kilku centurionów, paru legionistów, podstawowe umiejętności. Świeża baza to pusta kastra - a kohorta bez wyposażenia nie wyruszy na kampanię. Czas zatrudnić kwatermistrza, który zrobi to za nas: seeder.

Seeder a fixtures - kod i zapasy

Te dwa pojęcia lubią się mylić, więc rozdzielmy je od razu. Seeder to kod - klasa, która uruchomiona wypełnia bazę danymi startowymi. Fixtures to same dane - statyczne zestawy zapisane w plikach (JSON, YAML), które czekają, aż ktoś je wczyta. Mówiąc po rzymsku: fixtures to spis zapasów na pergaminie, seeder to kwatermistrz, który według tego spisu ładuje wozy. Seeder może mieć dane wpisane wprost w kodzie albo czytać je właśnie z pliku fixtures.

Pierwszy seeder

Skorzystamy z pakietu

typeorm-extension
, który dodaje do TypeORM obsługę seedów. Seeder to klasa implementująca interfejs
Seeder
z jedną metodą
run
- dostaje ona połączenie z bazą (
dataSource
), z którego bierzemy znane nam już repozytoria:

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}

Nic ponad to, co już umiesz: bierzemy repozytorium i wołamy

save
z tablicą obiektów. Cała nowość polega na opakowaniu: klasa z metodą
run
może zostać uruchomiona jednym poleceniem - na świeżej bazie, u każdego programisty, w każdym środowisku - i za każdym razem załaduje dokładnie to samo wyposażenie.

Czyszczenie i idempotentność - seeder odpalony dwa razy

Co się stanie, gdy ktoś uruchomi seeder ponownie? Z kodem powyżej - druga porcja tych samych centurionów i baza pełna duplikatów. Kwatermistrz musi być na to przygotowany, a strategie są dwie:

1public async run(dataSource: DataSource): Promise<void> {
2  const centurionRepository = dataSource.getRepository(Centurion);
3
4  // Strategia 1: wyczyść tabelę przed seedowaniem
5  await centurionRepository.clear();
6
7  // Strategia 2: jeśli dane już są, nie rób nic
8  const existing = await centurionRepository.count();
9  if (existing > 0) {
10    return;
11  }
12
13  await centurionRepository.save([ /* ... */ ]);
14}

W praktyce wybierasz jedną z nich.

clear()
opróżnia tabelę i seeduje od zera - wynik jest zawsze przewidywalny, więc to dobry wybór dla baz deweloperskich i testowych. Sprawdzenie
count()
zostawia istniejące dane w spokoju - bezpieczniejsze tam, gdzie baza mogła już zgromadzić coś cennego. Obie wersje mają wspólną cechę, o którą tu chodzi: seeder można uruchomić dowolnie wiele razy, a baza nie zamieni się w stos duplikatów. Tę odporność na powtórne uruchomienie nazywamy idempotentnością - i to polecam traktować jako żelazny wymóg każdego seedera, @name.

Kolejność ma znaczenie - relacje między tabelami

Legionista w naszym systemie należy do centuriona. To znaczy, że nie możemy zasiać legionistów do pustej bazy - ich rekordy wskazują na centurionów, którzy muszą istnieć wcześniej. Kwatermistrz ładuje wozy w ustalonej kolejności:

1// 1. Najpierw encje niezależne
2const centurions = await centurionRepository.save([
3  { name: 'Maximus Decimus', cohortName: 'Legio X Equestris' },
4]);
5
6// 2. Potem encje, które się do nich odwołują
7await legionaryRepository.save([
8  { name: 'Marek Antoniusz', rank: 'Speculator', centurion: centurions[0] },
9  { name: 'Gajusz Juliusz', rank: 'Optio', centurion: centurions[0] },
10]);

Zauważ, że

save
zwraca zapisane obiekty - już z nadanymi przez bazę identyfikatorami. Dzięki temu w drugim kroku możemy podać
centurions[0]
jako właściciela relacji. Ogólna reguła brzmi: najpierw tabele, na które wskazują inne (umiejętności, rangi, centurioni), potem tabele zależne (legioniści), na końcu dane wiążące jedno z drugim (tributy z przypisanym właścicielem). Czyszczenie wykonujemy w kolejności odwrotnej - najpierw zależnych, potem niezależnych.

Fabryka danych - setka legionistów na testy

Dane startowe wpisujemy ręcznie, bo jest ich kilka. Ale do testów wydajności albo pracy nad paginacją potrzebujesz setek rekordów - i nikt nie będzie ich wymyślał. Od tego jest fabryka: przepis na jeden losowy obiekt, który biblioteka

@faker-js/faker
wypełnia zmyślonymi danymi:

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});

Fabryka opisuje, jak wygląda jeden losowy legionista: prawdziwie brzmiące imię, ranga wylosowana z listy, licznik tributów z zadanego przedziału. Mnożenie to już jedna linijka w seederze:

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

I kastra pełna: sto różnych legionistów, każdy z innymi danymi, zero ręcznej roboty. Zwróć uwagę na podział ról - fabryka definiuje kształt pojedynczego rekordu, a dopiero seeder decyduje, ile ich powstanie i kiedy.

Uruchamianie - spinamy wszystko razem

Na koniec mówimy TypeORM, gdzie szukać seedów i fabryk. Rozszerzamy konfigurację źródła danych o dwa pola:

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};

Pole

seeds
wskazuje klasy seederów,
factories
- przepisy fabryk. Od tej chwili jedno polecenie w terminalu,
npm run seed:run
, przejdzie po wszystkich seedach i wyposaży bazę. Świeża kastra staje się gotowym obozem w kilka sekund.

Podsumowanie

Twoja kohorta nigdy więcej nie wyruszy bez zapasów:

  • seeder to kod ładujący dane startowe, fixtures to statyczne dane w plikach - spis zapasów i kwatermistrz, który go realizuje,
  • seeder musi być idempotentny: wyczyść tabelę przez
    clear()
    albo pomiń seedowanie, gdy
    count()
    wykryje istniejące dane,
  • przy relacjach kolejność jest święta: najpierw encje niezależne, potem zależne - a czyszczenie odwrotnie,
  • fabryka z fakerem opisuje jeden losowy rekord, a
    saveMany(100)
    mnoży go na potrzeby testów,
  • pola
    seeds
    i
    factories
    w konfiguracji spinają całość w jedno polecenie
    seed:run
    .

W następnej lekcji zajmiemy się walidacją danych - bo skoro skarbiec umie się już sam wyposażyć, pora zadbać, by nie trafiło do niego nic zepsutego. A na razie zapamiętaj: seeder to kwatermistrz kohorty - dzięki niemu każda świeża baza dostaje to samo, kompletne wyposażenie na jedno polecenie.

Przejdź do CodeWorlds