Dodałeś do encji
Legionary pole rank. U Ciebie działa - baza deweloperska sama dorobiła kolumnę. Wysyłasz kod, a nazajutrz przychodzi wiadomość od kolegi: aplikacja nie wstaje, bo w jego bazie kolumny rank nie ma. Na produkcji byłoby gorzej - tam nikt nie pozwoli bazie przebudowywać się samodzielnie, bo przy okazji potrafi skasować dane.Problem jest ten sam co z kodem, zanim wymyślono git: zmiany w strukturze bazy nie mają historii. Nie wiadomo, kto co zmienił, w jakiej kolejności i jak to cofnąć. Rzymianie prowadzili na to archiwum - każda zmiana w rejestrze legionu zapisana jako osobny dokument z datą. W TypeORM ten dokument nazywa się migracją.
Migracja to plik z jedną zmianą schematu bazy - dodaniem tabeli, kolumny, indeksu. Ma znacznik czasu w nazwie, więc migracje układają się w kolejność, a baza pamięta, które już wykonała. To wersjonowanie schematu: dokładnie to samo, co git robi z kodem, tylko dla struktury tabel.
Dzięki temu każdy kolega odpala u siebie te same migracje w tej samej kolejności i dostaje identyczną bazę. A na produkcji zmiana wchodzi świadomie, jednym poleceniem, a nie przez zgadywanie ORM-a. Dlatego
synchronize: true - opcja, która pozwala TypeORM przebudowywać bazę pod encje - jest wygodna lokalnie, ale nigdy nie powinna trafić na produkcję.Migracja to klasa implementująca interfejs
MigrationInterface z dwiema metodami:1import { MigrationInterface, QueryRunner } from 'typeorm';
2
3export class CreateLegionaryTable1710000000000 implements MigrationInterface {
4 public async up(queryRunner: QueryRunner): Promise<void> {
5 await queryRunner.query(`
6 CREATE TABLE "legionary" (
7 "id" SERIAL PRIMARY KEY,
8 "name" character varying(100) NOT NULL,
9 "rank" character varying(50)
10 )
11 `);
12 }
13
14 public async down(queryRunner: QueryRunner): Promise<void> {
15 await queryRunner.query(`DROP TABLE "legionary"`);
16 }
17}Te dwie metody to droga tam i z powrotem.
up() wprowadza zmianę - tu tworzy tabelę. down() ją cofa - usuwa tę samą tabelę. Zasada jest prosta: down() musi dokładnie odwracać to, co zrobiło up(). Dodałeś kolumnę w up? Usuń ją w down. Bez tego wycofanie migracji zostawi bazę w stanie, którego nikt nie przewidział.Zwróć uwagę na argument obu metod:
queryRunner. To ten sam obiekt, który poznasz jeszcze w lekcji o transakcjach - reprezentuje połączenie z bazą, na którym wykonujemy polecenia. Liczba w nazwie klasy (1710000000000) to znacznik czasu utworzenia; to po nim TypeORM ustala kolejność migracji, a nie po nazwie pliku.W codziennej pracy rzadko piszesz migracje ręcznie. Kolejność jest zawsze taka sama i warto ją zapamiętać jako cztery kroki:
1. Zmień encję - dodaj pole, zmień typ, usuń kolumnę. To jedyne miejsce, gdzie opisujesz, jak ma wyglądać baza.
2. Wygeneruj migrację poleceniem, które porówna encje ze stanem bazy i zapisze różnicę:
1typeorm migration:generate -n AddRankToLegionary3. Uruchom migrację - dopiero teraz zmiana trafia do bazy:
1typeorm migration:run4. Sprawdź wynik w bazie - czy kolumna jest, czy ma właściwy typ, czy dane ocalały.
Między krokiem drugim a trzecim jest miejsce, o którym łatwo zapomnieć: przeczytaj wygenerowany plik. Generator porównuje encje z bazą i czasem interpretuje zmianę inaczej, niż zamierzałeś - zamiast zmienić nazwę kolumny, potrafi usunąć starą i dodać nową, gubiąc po drodze wszystkie dane. Ten przegląd zajmuje pół minuty i to polecam jako nawyk, @name.
Warto rozdzielić dwa polecenia o mylnie podobnych nazwach.
migration:generate porównuje encje z bazą i wypełnia up() oraz down() za Ciebie - to komenda, której użyjesz w 95% przypadków. migration:create tworzy pusty szkielet migracji, który wypełniasz sam - przydaje się, gdy zmiana nie wynika z encji, na przykład przy przenoszeniu danych między kolumnami.Gdy migracja okaże się błędna, cofasz ją jednym poleceniem:
1typeorm migration:revertrevert wywołuje metodę down() ostatniej wykonanej migracji - jednej, nie wszystkich. Żeby cofnąć się o trzy kroki, wywołujesz je trzy razy. I tu widać, po co była ta dyscyplina przy pisaniu down(): cofnięcie zadziała dokładnie tak dobrze, jak dobrze opisałeś drogę powrotną.Skoro migracje powstają z encji, to opcje kolumn są tak naprawdę instrukcjami dla generatora. Kilka z nich warto znać, bo wprost decydują o kształcie tabeli:
1@Entity()
2export class Tribute {
3 @PrimaryGeneratedColumn()
4 id: number;
5
6 @Column({ type: 'varchar', length: 100 })
7 name: string;
8
9 @Column({ type: 'decimal', precision: 10, scale: 2 })
10 value: number;
11
12 @Column({ default: false })
13 isCursed: boolean;
14}@PrimaryGeneratedColumn() to klucz główny nadawany automatycznie przez bazę - nigdy nie ustawiasz go ręcznie. type: 'varchar' z length: 100 daje tekst o ograniczonej długości; próba zapisania dłuższej nazwy zostanie odrzucona przez bazę, a nie po cichu przycięta.Najciekawszy jest
decimal z parą precision i scale, bo ta para bywa myląca. precision to łączna liczba cyfr, a scale - ile z nich stoi po przecinku. Zapis precision: 10, scale: 2 oznacza więc osiem cyfr przed przecinkiem i dwie po nim, czyli wartości do 99 999 999,99. Dlaczego nie zwykły float? Bo liczby zmiennoprzecinkowe zaokrąglają - przy pieniądzach i wartościach tributów sumy przestałyby się zgadzać. decimal przechowuje dokładną wartość.default: false wpisuje wartość domyślną do definicji kolumny w bazie - nowy tribut będzie nieprzeklęty, nawet jeśli kod tego pola nie poda.Archiwum legionu prowadzi się samo, a Ty wiesz, jak je czytać:
synchronize: true bywa wygodne lokalnie, ale na produkcji potrafi skasować dane,MigrationInterface: up() wprowadza zmianę, down() musi ją dokładnie odwracać,migration:generate, przeczytaj wygenerowany plik, migration:run, sprawdź bazę,generate wypełnia migrację z różnicy encji i bazy, create daje pusty szkielet do napisania ręcznie,migration:revert cofa jedną, ostatnią migrację, wywołując jej down(),length ogranicza tekst, decimal z precision i scale przechowuje dokładne kwoty, default wpisuje wartość domyślną do bazy.W następnej lekcji połączymy tabele relacjami - bo legionista bez centuriona i tributu to wciąż samotny wpis w rejestrze. A na razie zapamiętaj: migracja to dokument w archiwum - opisuje jedną zmianę i drogę powrotną, dzięki czemu każda baza w Imperium może dojść do tego samego stanu.