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

Migracje i Entity - archiwum rzymskiego legionu

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

Czym jest migracja

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

Anatomia migracji: up i down

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.

Cykl pracy z migracjami

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 AddRankToLegionary

3. Uruchom migrację - dopiero teraz zmiana trafia do bazy:

1typeorm migration:run

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

generate a create - dwie różne komendy

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:revert

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

Encja steruje migracją - opcje kolumn

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.

Podsumowanie

Archiwum legionu prowadzi się samo, a Ty wiesz, jak je czytać:

  • migracja to wersjonowanie schematu bazy - git dla struktury tabel,
  • synchronize: true
    bywa wygodne lokalnie, ale na produkcji potrafi skasować dane,
  • klasa migracji implementuje
    MigrationInterface
    :
    up()
    wprowadza zmianę,
    down()
    musi ją dokładnie odwracać,
  • cykl pracy: zmień encję,
    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()
    ,
  • opcje kolumn sterują generatorem:
    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.

Przejdź do CodeWorlds