Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Migracje i Entity - archiwum rzymskiego legionu

młody zarządco! Konsul Caesar.js ma dla Ciebie nowe wyzwanie. Teraz gdy już umiesz tworzyć podstawowe mapy tributów (entity), czas nauczyć się jak bezpiecznie aktualizować strukturę swojej skarbca bez ryzyka utraty cennych danych!

Czym są migracje w świecie Rzymian?

Wyobraź sobie, że Twój legion przez lata gromadzi tributy w starym skarbcu. Pewnego dnia okazuje się, że potrzebujesz większego skarbca z dodatkowymi przegródkami. Nie możesz po prostu przenieść wszystkiego na łeb na szyję - musisz to zrobić ostrożnie, krok po kroku.

Migracje to właśnie taki plan reorganizacji skarbca:

  • Dokumentują każdą zmianę w strukturze bazy danych
  • Można je cofnąć jeśli coś pójdzie nie tak
  • Są wykonywane automatycznie na wszystkich fortach i bazach legionu
  • Zapewniają spójność między różnymi środowiskami

Generowanie pierwszej migracji

Konfiguracja CLI TypeORM

1# Instalacja CLI
2npm install -g @nestjs/cli
3npm install --save-dev ts-node
1// package.json - skrypty migracyjne
2{
3 "scripts": {
4 "migration:generate": "typeorm-ts-node-commonjs migration:generate",
5 "migration:run": "typeorm-ts-node-commonjs migration:run",
6 "migration:revert": "typeorm-ts-node-commonjs migration:revert"
7 }
8}

Konfiguracja data source

1// data-source.ts
2import { DataSource } from 'typeorm';
3import { Tribute } from './src/tribute/tribute.entity';
4import { Legionary } from './src/legionary/legionary.entity';
5
6export const AppDataSource = new DataSource({
7 type: 'postgres',
8 host: 'localhost',
9 port: 5432,
10 username: 'consul',
11 password: 'imperium123',
12 database: 'roman_empire',
13 entities: [Tribute, Legionary],
14 migrations: ['src/migrations/*.ts'],
15 synchronize: false, // WAŻNE: false w produkcji!
16});

Tworzenie zaawansowanej entity Legionary

1// legionary.entity.ts
2import {
3 Entity,
4 PrimaryGeneratedColumn,
5 Column,
6 CreateDateColumn,
7 UpdateDateColumn,
8 Index,
9} from 'typeorm';
10
11@Entity('legionaries')
12@Index(['name', 'cohort']) // Indeks dla szybszego wyszukiwania
13export class Legionary {
14 @PrimaryGeneratedColumn()
15 id: number;
16
17 @Column({ length: 100, unique: true })
18 @Index() // Indeks na nazwie
19 name: string;
20
21 @Column({ length: 100, nullable: true })
22 cohort: string;
23
24 @Column({ type: 'int', default: 0 })
25 tributeCount: number;
26
27 @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
28 totalTributeValue: number;
29
30 @Column({ length: 50 })
31 rank: string;
32
33 @Column({ type: 'text', nullable: true })
34 biography: string;
35
36 @Column({ type: 'json', nullable: true })
37 skills: {
38 navigation: number;
39 combat: number;
40 negotiation: number;
41 leadership: number;
42 };
43
44 @Column({ default: true })
45 isActive: boolean;
46
47 @CreateDateColumn()
48 joinedAt: Date;
49
50 @UpdateDateColumn()
51 lastUpdated: Date;
52
53 @Column({ type: 'date', nullable: true })
54 lastSeenAt: Date;
55}

Generowanie migracji po zmianie entity

1# Generowanie migracji na podstawie zmian w entity
2npm run migration:generate -- src/migrations/AddLegionaryEntity

TypeORM automatycznie wygeneruje plik migracji:

1// src/migrations/1234567890123-AddLegionaryEntity.ts
2import { MigrationInterface, QueryRunner } from 'typeorm';
3
4export class AddLegionaryEntity1234567890123 implements MigrationInterface {
5 name = 'AddLegionaryEntity1234567890123';
6
7 public async up(queryRunner: QueryRunner): Promise<void> {
8 await queryRunner.query(`
9 CREATE TABLE "legionaries" (
10 "id" SERIAL NOT NULL,
11 "name" character varying(100) NOT NULL,
12 "cohort" character varying(100),
13 "tributeCount" integer NOT NULL DEFAULT '0',
14 "totalTributeValue" numeric(10,2) NOT NULL DEFAULT '0',
15 "rank" character varying(50) NOT NULL,
16 "biography" text,
17 "skills" json,
18 "isActive" boolean NOT NULL DEFAULT true,
19 "joinedAt" TIMESTAMP NOT NULL DEFAULT now(),
20 "lastUpdated" TIMESTAMP NOT NULL DEFAULT now(),
21 "lastSeenAt" date,
22 CONSTRAINT "UQ_legionaries_name" UNIQUE ("name"),
23 CONSTRAINT "PK_legionaries_id" PRIMARY KEY ("id")
24 )
25 `);
26
27 await queryRunner.query(`
28 CREATE INDEX "IDX_legionaries_name" ON "legionaries" ("name")
29 `);
30
31 await queryRunner.query(`
32 CREATE INDEX "IDX_legionaries_name_cohort" ON "legionaries" ("name", "cohort")
33 `);
34 }
35
36 public async down(queryRunner: QueryRunner): Promise<void> {
37 await queryRunner.query(`DROP INDEX "IDX_legionaries_name_cohort"`);
38 await queryRunner.query(`DROP INDEX "IDX_legionaries_name"`);
39 await queryRunner.query(`DROP TABLE "legionaries"`);
40 }
41}

Uruchamianie migracji

1# Wykonanie wszystkich oczekujących migracji
2npm run migration:run
3
4# Cofnięcie ostatniej migracji (gdy coś pójdzie nie tak)
5npm run migration:revert

Ręczne tworzenie migracji

Czasami potrzebujesz stworzyć migracje ręcznie, np. do seedowania danych:

1// src/migrations/1234567890124-SeedInitialLegionaries.ts
2import { MigrationInterface, QueryRunner } from 'typeorm';
3
4export class SeedInitialLegionaries1234567890124 implements MigrationInterface {
5 public async up(queryRunner: QueryRunner): Promise<void> {
6 await queryRunner.query(`
7 INSERT INTO "legionaries" ("name", "cohort", "rank", "tributeCount", "totalTributeValue", "skills") VALUES
8 ('Marcus Aurelius', 'Legio X Equestris', 'Centurion', 150, 50000.00, '{"navigation": 95, "combat": 90, "negotiation": 70, "leadership": 95}'),
9 ('Julia Domna', 'Legio XII Fulminata', 'Optio', 45, 12000.00, '{"navigation": 70, "combat": 95, "negotiation": 80, "leadership": 75}'),
10 ('Lucius Verus', 'Legio III Gallica', 'Centurion', 60, 18000.00, '{"navigation": 80, "combat": 75, "negotiation": 85, "leadership": 70}'),
11 ('Hadrian', 'Legio XII Fulminata', 'Speculator', 35, 8500.00, '{"navigation": 90, "combat": 85, "negotiation": 60, "leadership": 65}')
12 `);
13 }
14
15 public async down(queryRunner: QueryRunner): Promise<void> {
16 await queryRunner.query(`
17 DELETE FROM "legionaries" WHERE "name" IN ('Marcus Aurelius', 'Julia Domna', 'Lucius Verus', 'Hadrian')
18 `);
19 }
20}

Zaawansowane typy kolumn

1@Entity('detailed_legionaries')
2export class DetailedLegionary {
3 @PrimaryGeneratedColumn('uuid') // UUID zamiast auto-increment
4 id: string;
5
6 @Column({ type: 'enum', enum: ['Centurion', 'Optio', 'Speculator', 'Tesserarius', 'Coquus'] })
7 rank: string;
8
9 @Column({ type: 'simple-array' }) // Tablica stringów
10 languages: string[];
11
12 @Column({ type: 'simple-json' }) // Prosty JSON
13 equipment: { weapon: string; armor: string; accessories: string[] };
14
15 @Column({ type: 'point' }) // Typ geometryczny PostgreSQL
16 lastKnownLocation: string;
17
18 @Column({ type: 'interval' }) // Przedział czasu
19 timeSinceLastVoyage: string;
20
21 @Column({ type: 'money' }) // Typ pieniężny PostgreSQL
22 bounty: string;
23}

Walidacja na poziomie bazy danych

1@Entity('validated_legionaries')
2@Check('"age" >= 16 AND "age" <= 100') // Walidacja wieku
3@Check('"tributeCount" >= 0') // Nie może być ujemna
4export class ValidatedLegionary {
5 @PrimaryGeneratedColumn()
6 id: number;
7
8 @Column({ length: 100 })
9 @Index({ unique: true }) // Unikalny indeks
10 name: string;
11
12 @Column({ type: 'int' })
13 age: number;
14
15 @Column({ type: 'int', default: 0 })
16 tributeCount: number;
17
18 @Column({ type: 'varchar', length: 100 })
19 @Index() // Zwykły indeks dla szybszego wyszukiwania
20 homePort: string;
21}

Migracja zmiany struktury

Przykład dodania nowej kolumny do istniejącej tabeli:

1// Dodanie pola 'reputation' do tabeli legionaries
2@Column({ type: 'int', default: 50 })
3reputation: number;
1npm run migration:generate -- src/migrations/AddReputationToLegionarys

Wygenerowana migracja:

1export class AddReputationToLegionarys1234567890125 implements MigrationInterface {
2 public async up(queryRunner: QueryRunner): Promise<void> {
3 await queryRunner.query(`
4 ALTER TABLE "legionaries" 
5 ADD "reputation" integer NOT NULL DEFAULT '50'
6 `);
7 }
8
9 public async down(queryRunner: QueryRunner): Promise<void> {
10 await queryRunner.query(`
11 ALTER TABLE "legionaries" 
12 DROP COLUMN "reputation"
13 `);
14 }
15}

Ścisza migracji w produkcji

Dla dużych tabel, migracje mogą być powolne. Oto kilka wskazówek:

1export class OptimizedMigration implements MigrationInterface {
2 public async up(queryRunner: QueryRunner): Promise<void> {
3 // 1. Dodaj kolumnę bez indeksu
4 await queryRunner.query(`
5 ALTER TABLE "legionaries" 
6 ADD "email" varchar(255)
7 `);
8
9 // 2. Wypełnij dane w mniejszych partiach
10 await queryRunner.query(`
11 UPDATE "legionaries" 
12 SET "email" = "name" || '@legionary.legion' 
13 WHERE "id" BETWEEN 1 AND 1000
14 `);
15
16 // 3. Dodaj ograniczenia po wypełnieniu danych
17 await queryRunner.query(`
18 ALTER TABLE "legionaries" 
19 ALTER COLUMN "email" SET NOT NULL
20 `);
21
22 // 4. Dodaj indeks na końcu
23 await queryRunner.query(`
24 CREATE UNIQUE INDEX CONCURRENTLY "IDX_legionaries_email" 
25 ON "legionaries" ("email")
26 `);
27 }
28}

Najlepsze praktyki migracji

1. Zawsze testuj migracje

1# Test na kopii bazy danych
2npm run migration:run
3npm run migration:revert
4npm run migration:run

2. Unikaj usuwania kolumn od razu

1// Pierwszy release: zaznacz jako deprecated
2@Column({ nullable: true, comment: 'DEPRECATED: will be removed in v2.0' })
3oldField: string;
4
5// Drugi release: usuń
6// @Column() oldField: string; // zakomentowane

3. Używaj transakcji

1public async up(queryRunner: QueryRunner): Promise<void> {
2 await queryRunner.startTransaction();
3 try {
4 await queryRunner.query('...');
5 await queryRunner.query('...');
6 await queryRunner.commitTransaction();
7 } catch (error) {
8 await queryRunner.rollbackTransaction();
9 throw error;
10 }
11}

Przydatne komendy CLI

1# Tworzenie pustej migracji
2npm run migration:create -- src/migrations/CustomMigration
3
4# Pokazanie statusu migracji
5npm run migration:show
6
7# Cofnięcie konkretnej ilości migracji
8npm run migration:revert -- --fake

Praktyczne ćwiczenie

Spróbuj stworzyć własną entity i migracje:

  1. Stwórz entity
    Legion
    z polami: name, type, capacity, consul
  2. Wygeneruj migracje
  3. Dodaj nowe pole
    foundedYear
    do Legion
  4. Wygeneruj i uruchom migracje aktualizującą
1// Twój kod tutaj
2@Entity('legions')
3export class Legion {
4 // Zaimplementuj entity wedlug wskazówek
5}

Podsumowanie

Teraz jesteś prawdziwym archiwistą rzymskim! Umiesz:

Generować migracje automatycznie z entity ✓ Tworzyć ręczne migracje do specjalnych zadań ✓ Używać zaawansowanych typów kolumn w PostgreSQL ✓ Dodawać indeksy i walidacje na poziomie bazy ✓ Bezpiecznie aktualizować strukturę w produkcji

Konsul Caesar.js może teraz spoć spokojnie - jego skarbiec jest w dobrych rękach!

Ir a CodeWorlds