Kurs NestJS · Moduł 3: TypeORM i bazy danych
Repository Pattern - organizacja skarbca
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, forRootkonfiguruje połączenie raz dla aplikacji,forFeatureprzydziela encje pojedynczemu modułowi,find()przyjmuje opcjewhere,relationsiorder- warunki poza kluczemwheresą 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 - zwracaundefined, 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");
118Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jak zarejestrować repozytorium encji w module NestJS?
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