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.
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.
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.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.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.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.
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.Twoja kohorta nigdy więcej nie wyruszy bez zapasów:
clear() albo pomiń seedowanie, gdy count() wykryje istniejące dane,saveMany(100) mnoży go na potrzeby testów,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.