Masz już tabelę legionistów i tabelę legionów. Każda z osobna trzyma się dobrze - ale skąd wiadomo, kto pod kim służy? Można by wpisać nazwę legionu w kolumnę tekstową przy każdym legioniście. Do pierwszej literówki: "Legio X Equestris" i "Legio X Equestriss" to dla bazy dwa różne legiony, a Ty właśnie rozbiłeś kohortę na dwie.
Rzymianie znali lepszy sposób. Legionista nie nosi przy sobie nazwy legionu - nosi jego numer. Jeden legion, jeden numer, wszyscy wskazują na ten sam wpis w rejestrze. To jest relacja: powiązanie oparte na identyfikatorze, a nie na przepisywanym tekście.
W tej lekcji poznasz trzy rodzaje takich więzów i nauczysz się je czytać ze strony kodu.
Zacznijmy od najczęstszego układu. Legion ma wielu legionistów; legionista należy do jednego legionu. Ta relacja ma dwie strony i obie trzeba opisać - każdą w swojej encji:
1@Entity()
2export class Legion {
3 @OneToMany(() => Legionary, (legionary) => legionary.legion)
4 legionaries: Legionary[];
5}
6
7@Entity()
8export class Legionary {
9 @ManyToOne(() => Legion, (legion) => legion.legionaries)
10 legion: Legion;
11}Kluczowa jest zasada, po której poznasz, gdzie co postawić: dekorator opisuje stronę, na której stoi. Legion jest "jednym", a ma "wielu" legionistów - więc w encji
Legion stoi @OneToMany, a pole jest tablicą. Legionista jest jednym z "wielu" i należy do "jednego" legionu - w encji Legionary stoi @ManyToOne, a pole jest pojedyncze. Jeśli kiedykolwiek się zawahasz, przeczytaj zdanie od strony encji, którą właśnie piszesz: "legionista należy do jednego legionu" - i masz @ManyToOne.Przyjrzyjmy się dwóm argumentom dekoratora, bo oba są obowiązkowe i oba mylą. Pierwszy,
() => Legionary, wskazuje encję po drugiej stronie. Dlaczego funkcja, a nie sama nazwa klasy? Bo obie encje odwołują się do siebie nawzajem, a przy takim zapętleniu jedna z klas nie byłaby jeszcze zdefiniowana w chwili odczytu. Funkcja odracza to sięgnięcie do momentu, gdy obie już istnieją.Drugi argument,
(legionary) => legionary.legion, to strona odwrotna: wskazuje pole w tamtej encji, które opisuje tę samą relację. Dzięki niemu TypeORM wie, że Legion.legionaries i Legionary.legion to dwa końce jednego mostu, a nie dwie niezależne relacje.A co dzieje się w bazie? Kolumna z identyfikatorem powstaje tylko po stronie
- w tabeli legionistów pojawi się @ManyToOne
legionId. Tabela legionów nie zmienia się wcale. To jest ten "numer noszony przy sobie": każdy legionista wskazuje na swój legion, a nie odwrotnie.Drugi układ: legionista opanował wiele umiejętności, a każdą umiejętność opanowało wielu legionistów. Tu żadna ze stron nie może przechowywać identyfikatora - jedna kolumna nie pomieści listy.
1@Entity()
2export class Legionary {
3 @ManyToMany(() => Skill)
4 @JoinTable()
5 skills: Skill[];
6}
7
8@Entity()
9export class Skill {
10 @ManyToMany(() => Legionary, (legionary) => legionary.skills)
11 legionaries: Legionary[];
12}Rozwiązaniem jest osobna tabela pośrednia, w której jeden wiersz to jedna para: ten legionista zna tę umiejętność. Tworzy ją dla Ciebie dekorator
@JoinTable() - i to jest szczegół, na którym potyka się najwięcej osób: @JoinTable() stawiamy tylko po jednej stronie, tej, którą uznajemy za właścicielkę relacji. Postawienie go po obu stronach kończy się dwiema tabelami pośrednimi opisującymi to samo. Postawienie po żadnej - błędem przy starcie aplikacji.Zwróć uwagę, że
@ManyToMany po stronie Legionary obywa się bez drugiego argumentu. Strona odwrotna jest opcjonalna: podajesz ją wtedy, gdy chcesz też z drugiej strony sięgać po powiązania - tu z umiejętności odczytać, kto ją posiadł.Trzeci układ jest najprostszy: legionista ma jedną osobistą mapę tributów, a mapa należy do jednego legionisty.
1@Entity()
2export class Legionary {
3 @OneToOne(() => TributeMap)
4 @JoinColumn()
5 map: TributeMap;
6}Tu decyzję o tym, w której tabeli wyląduje kolumna z identyfikatorem, podejmujesz sam - wskazuje ją
@JoinColumn(). Postawiony w encji Legionary sprawia, że to tabela legionistów dostanie kolumnę mapId. Nie myl go z @JoinTable() z poprzedniej sekcji: @JoinColumn wskazuje kolumnę w istniejącej tabeli, @JoinTable tworzy całą nową tabelę pośrednią.Relacja opisana w encji nie znaczy, że dane przyjdą same. Domyślnie
find() przynosi sam rekord, a pole relacji zostaje puste - i to celowo, bo inaczej pobranie jednego legionisty ciągnęłoby za sobą pół bazy. Powiązania dociągasz świadomie:1const legionaries = await this.legionaryRepo.find({
2 relations: ['legion', 'skills'],
3});Klucz
relations wylicza, co ma dojechać razem z rekordem. To jest jawne ładowanie i domyślny sposób pracy: w każdym zapytaniu decydujesz osobno, czego potrzebujesz.Jest też wariant automatyczny. Dopisanie
eager: true do relacji sprawia, że dociągnie się ona zawsze, bez proszenia:1@ManyToOne(() => Legion, (legion) => legion.legionaries, { eager: true })
2legion: Legion;Wygodne - i właśnie dlatego niebezpieczne.
eager działa w każdym zapytaniu o legionistów, także tam, gdzie legion jest zupełnie niepotrzebny, i po cichu obciąża zapytania w całej aplikacji. Zauważ też, czego eager nie zmienia: to nadal jedno zapytanie z JOIN-em, tylko wykonywane zawsze zamiast na żądanie. Dlatego polecam trzymać się relations i sięgać po eager wyjątkowo, @name - gdy relacja naprawdę jest potrzebna za każdym razem.Legion trzyma się razem, a Ty umiesz opisać jego więzy:
@OneToMany po stronie "jednego" (pole tablicowe), @ManyToOne po stronie "wielu" (pole pojedyncze),() => Encja - odracza odczyt klasy, bo encje wskazują na siebie nawzajem,@ManyToOne,@ManyToMany wymaga tabeli pośredniej: @JoinTable() tylko po jednej stronie,@OneToOne z @JoinColumn() decyduje, w której tabeli wyląduje kolumna - @JoinColumn wskazuje kolumnę, @JoinTable tworzy nową tabelę,relations: ['...']; eager: true robi to zawsze i dlatego stosuj go oszczędnie.W następnej lekcji poznasz repozytoria - archiwistów, którzy sięgają po te dane w imieniu serwisów. A na razie zapamiętaj: relacja to numer noszony przy sobie - jeden wpis w rejestrze, na który wskazują wszyscy, zamiast nazwy przepisywanej przy każdym legioniście.