Kurs NestJS · Moduł 3: TypeORM i bazy danych

Migracje i Entity - archiwum rzymskiego legionu

5 min czytania
W tej lekcji6

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.

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.

Kod do tej lekcji: src/migrations.ts
1// Migracje i Entity - Archiwum Rzymskiego Legionu
2import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm';
3
4console.log("Migracje - bezpieczna reorganizacja skarbca!");
5
6// ===========================================
7// 1. Tworzenie tabeli - pierwsza migracja
8// ===========================================
9
10export class CreateLegionaries1700000000000 implements MigrationInterface {
11  async up(queryRunner: QueryRunner): Promise<void> {
12    await queryRunner.createTable(
13      new Table({
14        name: 'legionaries',
15        columns: [
16          {
17            name: 'id',
18            type: 'integer',
19            isPrimary: true,
20            isGenerated: true,
21            generationStrategy: 'increment',
22          },
23          { name: 'name', type: 'varchar', isNullable: false },
24          { name: 'rank', type: 'varchar', isNullable: false },
25          { name: 'experience', type: 'integer', default: '0' },
26          { name: 'legion', type: 'varchar', isNullable: false },
27          { name: 'created_at', type: 'timestamp', default: 'now()' },
28        ],
29      }),
30    );
31    console.log('Tabela legionaries utworzona!');
32  }
33
34  async down(queryRunner: QueryRunner): Promise<void> {
35    await queryRunner.dropTable('legionaries');
36    console.log('Tabela legionaries usunieta!');
37  }
38}
39
40// ===========================================
41// 2. Dodanie kolumny - kolejna migracja
42// ===========================================
43
44export class AddProvinceToLegionaries1700000001000 implements MigrationInterface {
45  async up(queryRunner: QueryRunner): Promise<void> {
46    await queryRunner.addColumn(
47      'legionaries',
48      new TableColumn({
49        name: 'province',
50        type: 'varchar',
51        isNullable: true,
52        default: "'Roma'",
53      }),
54    );
55    console.log('Dodano kolumne province!');
56  }
57
58  async down(queryRunner: QueryRunner): Promise<void> {
59    await queryRunner.dropColumn('legionaries', 'province');
60  }
61}
62
63// ===========================================
64// 3. Komendy migracji
65// ===========================================
66
67// npx typeorm migration:generate -n CreateLegionaries
68// npx typeorm migration:run       // Wykonaj migracje
69// npx typeorm migration:revert    // Cofnij ostatnia
70// npx typeorm migration:show      // Pokaz status
71
72// ===========================================
73// 4. Konfiguracja migracji
74// ===========================================
75
76// ormconfig.ts / data-source.ts
77// export const dataSource = new DataSource({
78//   type: 'postgres',
79//   host: 'localhost',
80//   port: 5432,
81//   migrations: ['src/migrations/*.ts'],
82//   migrationsTableName: 'typeorm_migrations',
83// });
84
85console.log("\n=== PODSUMOWANIE MIGRACJI ===");
86console.log("up() - wykonuje zmiane (tworzenie tabeli, dodanie kolumny)");
87console.log("down() - cofa zmiane (usuwanie tabeli, kolumny)");
88console.log("migration:run - wykonuje oczekujace migracje");
89console.log("migration:revert - cofa ostatnia migracje");
90console.log("synchronize: true TYLKO w development!");
91

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Do czego służą migracje w TypeORM?

  2. 2. Jaką komendą generujemy migrację na podstawie zmian w encjach TypeORM?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Stwórz migrację CreateLegionaryTable z metodą up() tworzącą tabelę i down() usuwającą ją

  • Układanie w pionie

    Uporządkuj kroki procesu migracji bazy danych od pierwszego do ostatniego:

  • Klikanie w kolejności

    Ułóż elementy komendy generowania migracji w prawidłowej kolejności:

  • Edytor kodu

    Napisz encję Tribute z @Column({ type: 'varchar', length: 100 }), @Column({ type: 'decimal', precision: 10, scale: 2 }) i @Column({ default: false })

Przydatne artykuły