Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Relacje w bazie danych - więzy legionu

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.

Jeden do wielu - legion i jego legioniści

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

@ManyToOne
- w tabeli legionistów pojawi się
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.

Wiele do wielu - legioniści i umiejętności

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

Jeden do jednego - legionista i jego mapa

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

Pobieranie relacji

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.

Podsumowanie

Legion trzyma się razem, a Ty umiesz opisać jego więzy:

  • relacja łączy tabele przez identyfikator, nie przez przepisywany tekst,
  • dekorator opisuje stronę, na której stoi:
    @OneToMany
    po stronie "jednego" (pole tablicowe),
    @ManyToOne
    po stronie "wielu" (pole pojedyncze),
  • pierwszy argument to funkcja
    () => Encja
    - odracza odczyt klasy, bo encje wskazują na siebie nawzajem,
  • drugi argument wskazuje stronę odwrotną, czyli drugi koniec tego samego mostu,
  • kolumna z identyfikatorem powstaje po stronie
    @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ę,
  • relacje pobierasz jawnie przez
    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.

Przejdź do CodeWorlds