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!
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:
1# Instalacja CLI
2npm install -g @nestjs/cli
3npm install --save-dev ts-node1// 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}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});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}1# Generowanie migracji na podstawie zmian w entity
2npm run migration:generate -- src/migrations/AddLegionaryEntityTypeORM 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}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:revertCzasami 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}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}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}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/AddReputationToLegionarysWygenerowana 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}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}1# Test na kopii bazy danych
2npm run migration:run
3npm run migration:revert
4npm run migration:run1// 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; // zakomentowane1public 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}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 -- --fakeSpróbuj stworzyć własną entity i migracje:
Legion z polami: name, type, capacity, consulfoundedYear do Legion1// Twój kod tutaj
2@Entity('legions')
3export class Legion {
4 // Zaimplementuj entity wedlug wskazówek
5}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!