Kurs NestJS · Moduł 3: TypeORM i bazy danych

Repository Pattern - organizacja skarbca

5 min czytania
W tej lekcji6

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.

Trzy kroki do repozytorium

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.

Odczyt - find z opcjami

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.

Zapis - create i save

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.

Aktualizacja - preload

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ę.

Usuwanie - remove i delete

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: remove tam, gdzie liczy się poprawność i reakcja na brak rekordu, delete tam, gdzie liczy się szybkość.

Podsumowanie

Skarbiec ma archiwistę i nikt nie schodzi do piwnicy na własną rękę:

  • repozytorium oddziela logikę biznesową od sposobu przechowywania danych,
  • droga jest zawsze trzystopniowa: encja z @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.

Kod do tej lekcji: src/repository-pattern.ts
1// Repository Pattern - Organizacja Skarbca
2import { Injectable, NotFoundException } from '@nestjs/common';
3import { InjectRepository } from '@nestjs/typeorm';
4import { Repository } from 'typeorm';
5
6console.log("Repository Pattern - zarzadca dostepu do danych!");
7
8// ===========================================
9// 1. Podstawowy serwis z repozytorium
10// ===========================================
11
12// Zakladamy encje Legionary z poprzedniej lekcji
13
14@Injectable()
15export class LegionaryService {
16  constructor(
17    @InjectRepository('Legionary')
18    private legionaryRepo: Repository<any>,
19  ) {}
20
21  // Pobierz wszystkich
22  async findAll() {
23    return this.legionaryRepo.find();
24  }
25
26  // Pobierz po 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  // Utworz nowego
36  async create(data: any) {
37    const legionary = this.legionaryRepo.create(data);
38    return this.legionaryRepo.save(legionary);
39  }
40
41  // Zaktualizuj
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  // Usun
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  // Znajdz po rangu
57  async findByRank(rank: string) {
58    return this.legionaryRepo.find({ where: { rank } });
59  }
60
61  // Zlicz legionistow w legionie
62  async countByLegion(legionName: string) {
63    return this.legionaryRepo.count({
64      where: { legion: legionName },
65    });
66  }
67}
68
69// ===========================================
70// 2. CRUD serwis z garnizonem
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// Rejestracja w module
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=== PODSUMOWANIE REPOSITORY ===");
112console.log("@InjectRepository(Entity) - wstrzyknij repozytorium");
113console.log("repo.find() - pobierz wszystkie rekordy");
114console.log("repo.findOneBy() - pobierz jeden rekord");
115console.log("repo.save() - zapisz (insert lub update)");
116console.log("repo.delete() - usun rekord");
117console.log("repo.count() - zlicz rekordy");
118

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jak zarejestrować repozytorium encji w module NestJS?

  2. 2. Jak znaleźć wszystkich legionistów z rangą 'Centurion' używając repozytorium TypeORM?

Zadania praktyczne w grze

  • Układanie w pionie

    Uporządkuj kroki konfiguracji repozytorium TypeORM w module NestJS:

  • Edytor kodu

    Napisz LegionaryService z @InjectRepository(Legionary) i metodami findAll(), findOne(id), create(dto) i remove(id)

  • Układanie w poziomie

    Ułóż elementy wstrzyknięcia repozytorium w konstruktorze serwisu:

  • Klikanie w kolejności

    Ułóż elementy zapisywania nowej encji przez repozytorium:

  • Edytor kodu

    Napisz serwis z metodami: find() z where i relations, create() z save, update() z preload i save, delete() z remove

Przydatne artykuły