Kurs NestJS · Moduł 3: TypeORM i bazy danych
Migracje i Entity - archiwum rzymskiego legionu
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 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.
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: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ą.
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: truebywa 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ę, generatewypełnia migrację z różnicy encji i bazy,createdaje pusty szkielet do napisania ręcznie,migration:revertcofa jedną, ostatnią migrację, wywołując jejdown(),- opcje kolumn sterują generatorem:
lengthogranicza tekst,decimalzprecisioniscaleprzechowuje dokładne kwoty,defaultwpisuje 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!");
91Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Do czego służą migracje w TypeORM?
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 })