Znasz już encje i relacje - skarbiec ma spisane regały i wiadomo, co z czym się łączy. Ale kto właściwie do niego sięga? Gdyby każdy serwis w Imperium sam pisał zapytania SQL, ta sama operacja "znajdź legionistę po imieniu" powstałaby w pięciu miejscach, w pięciu wersjach, z pięcioma różnymi błędami.
Rzymianie rozwiązali to inaczej: do skarbca ma dostęp archiwista. Serwis nie schodzi do piwnicy z pochodnią - mówi archiwiście, czego szuka, a ten wie, na której półce to leży. W kodzie tym archiwistą jest repozytorium: warstwa, która oddziela logikę biznesową od sposobu, w jaki dane są przechowywane.
TypeORM daje repozytorium za darmo dla każdej encji - wystarczy je poprosić. Droga jest zawsze ta sama i warto ją zapamiętać jako sekwencję.
Krok pierwszy: encja. To ją poznałeś w poprzednich lekcjach - klasa z dekoratorem
@Entity(), opisująca regał w skarbcu. Bez niej nie ma czego archiwizować.Krok drugi: rejestracja w module. Mówimy NestJS, dla których encji ma przygotować archiwistów:
1// legionary.module.ts
2@Module({
3 imports: [TypeOrmModule.forFeature([Legionary])],
4 providers: [LegionaryService],
5 controllers: [LegionaryController],
6})
7export class LegionaryModule {}Zwróć uwagę na nazwę:
forFeature, nie forRoot. Ta różnica jest częstym źródłem pomyłek, więc rozdzielmy je raz na zawsze. forRoot wywołujesz raz, w module głównym - konfiguruje połączenie z bazą dla całej aplikacji. forFeature wywołujesz w każdym module z osobna i wyliczasz w nim tylko te encje, których ten moduł faktycznie używa. Jeden ustawia drogę do skarbca, drugi przydziela archiwistów konkretnej kohorcie.Krok trzeci: wstrzyknięcie do serwisu. Zarejestrowanego archiwistę odbieramy w konstruktorze:
1@Injectable()
2export class LegionaryService {
3 constructor(
4 @InjectRepository(Legionary)
5 private legionaryRepo: Repository<Legionary>,
6 ) {}
7}Dekorator
@InjectRepository(Legionary) mówi NestJS, o którego archiwistę prosimy - bo w aplikacji jest ich wielu, po jednym na encję. Typ Repository<Legionary> to zapis generyczny: Repository to ogólny rodzaj archiwisty, a <Legionary> zawęża go do tej jednej encji. Dzięki temu TypeScript wie, że find() zwróci legionistów, a nie cokolwiek - i podpowie Ci pola przy pisaniu kodu.Zapamiętaj tę kolejność: encja,
forFeature w module, @InjectRepository w serwisie. Pominięcie środkowego kroku daje przy starcie aplikacji błąd o nieznanym providerze - i to najczęstsza przyczyna tego komunikatu.Archiwista przyjmuje polecenia w postaci obiektu opcji. Najważniejszy klucz to
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() bez argumentów przynosi wszystko. Z obiektem opcji - zawęża. Uwaga na jeden szczegół, na którym potyka się wielu: warunki muszą siedzieć w kluczu where. Zapis find({ rank: 'Centurion' }) nie jest filtrem - to nieznana opcja, którą TypeORM zignoruje, i dostaniesz wszystkich legionistów zamiast samych centurionów. Błąd nie zostanie zgłoszony, więc łatwo go przeoczyć.Klucz
relations dociąga powiązane encje - to repozytoryjny odpowiednik leftJoinAndSelect z Query Buildera. order sortuje. Gdy potrzebujesz jednego rekordu, wołasz findOne({ where: { id } }) - zwróci encję albo null, jeśli nic nie pasuje.Tworzenie nowego legionisty to para metod, i warto rozumieć, dlaczego dwie, a nie jedna:
1async create(dto: CreateLegionaryDto): Promise<Legionary> {
2 const legionary = this.legionaryRepo.create(dto);
3 return this.legionaryRepo.save(legionary);
4}create() nie dotyka bazy. Buduje tylko obiekt encji w pamięci - przepisuje pola z DTO i nadaje mu klasę Legionary, dzięki czemu zadziałają dekoratory, hooki i wartości domyślne. Dopiero save() wysyła go do skarbca i zwraca zapisaną encję, już z nadanym przez bazę id.Ten podział ma sens praktyczny: między
create a save możesz jeszcze coś zmienić albo sprawdzić. save() ma też drugą twarz - wywołany na encji, która ma już id, wykona aktualizację zamiast wstawienia. Jedna metoda, dwa zachowania, zależnie od tego, czy rekord istnieje.Do zmiany istniejącego rekordu służy
preload, metoda o nieoczywistej nazwie: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(`Legionista ${id} nie istnieje`);
6 }
7
8 return this.legionaryRepo.save(legionary);
9}preload pobiera z bazy rekord o podanym id, nakłada na niego pola z DTO i zwraca gotową do zapisania encję - ale jeszcze nic nie zapisuje. Zaletą jest to, czego nie musisz robić: pola nieobecne w DTO zachowają swoje dotychczasowe wartości, więc częściowa aktualizacja nie wyzeruje reszty rekordu. Gdy rekordu o takim id nie ma, preload zwraca undefined - stąd sprawdzenie przed zapisem. Dopiero save() utrwala zmianę.Na koniec dwie metody usuwania, które robią to samo w różny sposób:
1async remove(id: number): Promise<void> {
2 const legionary = await this.legionaryRepo.findOne({ where: { id } });
3
4 if (!legionary) {
5 throw new NotFoundException(`Legionista ${id} nie istnieje`);
6 }
7
8 await this.legionaryRepo.remove(legionary);
9}remove() przyjmuje encję - dlatego najpierw ją pobieramy. Kosztuje to dodatkowe zapytanie, ale w zamian dostajesz pewność, że rekord istniał, i uruchamiasz hooki usuwania. delete(id) przyjmuje samo id i kasuje jednym zapytaniem - szybciej, ale bez sprawdzenia i bez hooków. Wybierz świadomie, @name: remove tam, gdzie liczy się poprawność i reakcja na brak rekordu, delete tam, gdzie liczy się szybkość.Skarbiec ma archiwistę i nikt nie schodzi do piwnicy na własną rękę:
@Entity(), TypeOrmModule.forFeature([Encja]) w module, @InjectRepository(Encja) w konstruktorze serwisu,forRoot konfiguruje połączenie raz dla aplikacji, forFeature przydziela encje pojedynczemu modułowi,find() przyjmuje opcje where, relations i order - warunki poza kluczem where są po cichu ignorowane,create() buduje encję w pamięci, save() zapisuje ją do bazy i potrafi też aktualizować,preload() scala istniejący rekord ze zmianami, nie zapisując - zwraca undefined, gdy rekordu nie ma,remove(encja) usuwa bezpieczniej i z hookami, delete(id) szybciej i bez nich.W następnej lekcji poznasz Query Builder - narzędzie na pytania, których archiwista nie potrafi obsłużyć samym
find(). A na razie zapamiętaj: repozytorium to archiwista skarbca - serwis mówi, czego potrzebuje, a nie jak to wyjąć z półki.