Database Migrations (TypeORM)

~20 daq · NestJS · Ma'lumotlar bazasi

Muammodan boshlaylik

Siz loyiha yozdingiz. synchronize: true bilan ishlayapsiz — entity o'zgarganda jadval avtomatik yangilanadi. Hammasi zo'r.

Keyin loyiha production ga chiqadi. Aytaylik, users jadvaliga yangi phone ustunini qo'shish kerak. Siz entity ga qo'shasiz:

@Column({ nullable: true })
phone: string;

synchronize: true production da yoqilgan bo'lsa — server qayta ishga tushganda TypeORM jadvalga phone ustunini qo'shadi. Bu yaxshi.

Lekin endi boshqa holat: siz name ustunini firstName va lastName ga ajratmoqchisiz.

// Eski
@Column()
name: string;

// Yangi
@Column()
firstName: string;

@Column()
lastName: string;

synchronize: true nima qiladi?

❌ Production da bo'ladigan narsa:

  1. name ustuni o'chiriladi          ← 50,000 foydalanuvchining ismi yo'qoladi
  2. firstName ustuni qo'shiladi      ← bo'sh
  3. lastName ustuni qo'shiladi       ← bo'sh

  Ma'lumotlar yo'qoldi. Tiklab bo'lmaydi.

Aynan shu muammo uchun migration kerak.


Migration nima?

Migration — ma'lumotlar bazasiga qilinadigan o'zgarishlarni bosqichma-bosqich, nazorat ostida amalga oshirish usuli.

Hayotiy o'xshatma bilan tushuntiraman: ko'chmas mulk agenti binoni ta'mirlaydi. U har bir ishni qayd qiladi — "5-may: devorni bo'yadim, 6-may: deraza oldim, 7-may: parket yoqdim". Agar biror narsa noto'g'ri ketsa — "7-may dagi ishni qaytaraman" deb parketni ko'taradi.

Migration ham xuddi shunday — har bir o'zgarish qayd qilinadi. Kerak bo'lsa — orqaga qaytariladi.

  synchronize: true             Migration
  ─────────────────────────────────────────────────────
  Avtomatik, nazorat yo'q    │  Qo'lda, nazorat bor
  Development uchun          │  Production uchun
  Ma'lumot yo'qolishi mumkin │  Ma'lumot saqlanadi
  Orqaga qaytarib bo'lmaydi  │  Orqaga qaytarish mumkin
  Git da saqlanmaydi         │  Git da saqlanadi (tarix)

Migration qanday ishlaydi?

  migrations jadvali (PostgreSQL da)
  ┌─────┬────────────────────────────────────┬─────────────────────┐
  │ id  │ name                               │ timestamp           │
  ├─────┼────────────────────────────────────┼─────────────────────┤
  │  1  │ 1700000000000-CreateUsersTable     │ 2024-11-15 10:00:00 │
  │  2  │ 1700000001000-AddPhoneToUsers      │ 2024-11-15 10:01:00 │
  │  3  │ 1700000002000-CreatePostsTable     │ 2024-11-16 09:00:00 │
  └─────┴────────────────────────────────────┴─────────────────────┘

  TypeORM bu jadvalga qarab: "qaysi migrationlar allaqachon bajarilgan?"
  deb biladi. Yangi migration fayllar bajarilmagan — ularni ishga tushiradi.

Har bir migration faylida ikkita metod bo'ladi:

  • up() — o'zgarishni qo'llash (oldinga)
  • down() — o'zgarishni bekor qilish (orqaga)

O'rnatish va sozlash

1. typeorm-cli uchun alohida konfiguratsiya

TypeORM CLI — migration yaratish va ishga tushirish uchun buyruq qatori vositasi. U alohida konfiguratsiya fayl kerak qiladi:

// data-source.ts  ← loyiha ildizida (src/ tashqarisida)
import { DataSource } from 'typeorm';
import * as dotenv from 'dotenv';

dotenv.config();   // .env ni o'qiydi

export const AppDataSource = new DataSource({
  type: 'postgres',
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT) || 5432,
  username: process.env.DB_USERNAME || 'postgres',
  password: process.env.DB_PASSWORD || '',
  database: process.env.DB_NAME || 'mydb',

  entities: ['src/**/*.entity{.ts,.js}'],
  migrations: ['src/migrations/*{.ts,.js}'],
  // migrations — migration fayllar qayerda saqlanadigini ko'rsatadi
});

Bu fayl app.module.ts dagi konfiguratsiyadan farq qiladi — faqat CLI uchun.

npm install dotenv

2. package.json ga skriptlar qo'shish

{
  "scripts": {
    "migration:generate": "typeorm-ts-node-commonjs migration:generate -d data-source.ts",
    "migration:create":   "typeorm-ts-node-commonjs migration:create",
    "migration:run":      "typeorm-ts-node-commonjs migration:run -d data-source.ts",
    "migration:revert":   "typeorm-ts-node-commonjs migration:revert -d data-source.ts",
    "migration:show":     "typeorm-ts-node-commonjs migration:show -d data-source.ts"
  }
}

3. app.module.ts da synchronize o'chirish

TypeOrmModule.forRootAsync({
  useFactory: (configService: ConfigService) => ({
    // ...
    synchronize: false,   // ← endi migration ishlatamiz
    migrations: ['dist/migrations/*{.ts,.js}'],
    migrationsRun: true,
    // migrationsRun: true — ilova ishga tushganda migrationlar avtomatik bajariladi
  }),
  inject: [ConfigService],
}),

Migration yaratish — ikkita usul

Usul 1: generate — avtomatik

Bu eng qulay usul. TypeORM entity lar va hozirgi baza holati orasidagi farqni o'zi topadi va migration yozadi.

npm run migration:generate -- src/migrations/CreateUsersTable
  Entity:              Baza:              TypeORM nima qiladi?
  ──────────────────────────────────────────────────────────
  users jadvali bor  │ Baza bo'sh       │ CREATE TABLE users yozadi
  phone ustuni bor   │ phone yo'q       │ ALTER TABLE ADD COLUMN phone yozadi
  name ustuni yo'q   │ name bor         │ ALTER TABLE DROP COLUMN name yozadi

Misol: User entity yaratdingiz — generate buyrug'i quyidagilarni yozadi:

// src/migrations/1700000000000-CreateUsersTable.ts
import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateUsersTable1700000000000 implements MigrationInterface {

  public async up(queryRunner: QueryRunner): Promise<void> {
    // up() — bu migration ishga tushganda nima qilinadi
    await queryRunner.query(`
      CREATE TABLE "users" (
        "id"        SERIAL PRIMARY KEY,
        "name"      character varying(100) NOT NULL,
        "email"     character varying      NOT NULL UNIQUE,
        "password"  character varying      NOT NULL,
        "role"      "user_role_enum"        NOT NULL DEFAULT 'user',
        "isActive"  boolean                NOT NULL DEFAULT true,
        "createdAt" TIMESTAMP              NOT NULL DEFAULT now(),
        "updatedAt" TIMESTAMP              NOT NULL DEFAULT now(),
        "deletedAt" TIMESTAMP
      )
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    // down() — bu migration orqaga qaytarilganda nima qilinadi
    await queryRunner.query(`DROP TABLE "users"`);
  }
}

Usul 2: create — qo'lda

Ba'zan TypeORM avtomatik topa olmaydigan o'zgarishlar bo'ladi — masalan, name ustunini firstName va lastName ga ajratish, ma'lumotlarni ko'chirish, index qo'shish.

npm run migration:create -- src/migrations/SplitNameColumn

Bu bo'sh migration fayl yaratadi — ichini o'zingiz to'ldirasiz:

// src/migrations/1700000003000-SplitNameColumn.ts
import { MigrationInterface, QueryRunner } from 'typeorm';

export class SplitNameColumn1700000003000 implements MigrationInterface {

  public async up(queryRunner: QueryRunner): Promise<void> {
    // 1. Yangi ustunlar qo'shamiz
    await queryRunner.query(`
      ALTER TABLE "users"
      ADD COLUMN "firstName" character varying,
      ADD COLUMN "lastName"  character varying
    `);

    // 2. Mavjud ma'lumotlarni ko'chiramiz — name ni bo'sh joy bo'yicha ajratamiz
    await queryRunner.query(`
      UPDATE "users"
      SET
        "firstName" = SPLIT_PART(name, ' ', 1),
        "lastName"  = SPLIT_PART(name, ' ', 2)
    `);

    // 3. Eski ustunni o'chiramiz
    await queryRunner.query(`
      ALTER TABLE "users" DROP COLUMN "name"
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    // Orqaga qaytarish — firstName + lastName ni name ga birlashtiramiz
    await queryRunner.query(`
      ALTER TABLE "users" ADD COLUMN "name" character varying
    `);

    await queryRunner.query(`
      UPDATE "users"
      SET "name" = CONCAT("firstName", ' ', "lastName")
    `);

    await queryRunner.query(`
      ALTER TABLE "users"
      DROP COLUMN "firstName",
      DROP COLUMN "lastName"
    `);
  }
}

Bu yondashuv bilan ma'lumot yo'qolmaydi — avval yangi ustunlar yaratiladi, ma'lumot ko'chiriladi, keyin eski ustun o'chiriladi.


Migration buyruqlari

migration:run — migrationlarni bajarish

npm run migration:run
  Nima bo'ladi:

  1. TypeORM migrations jadvalini tekshiradi
  2. Bajarilmagan migration fayllarni topadi
  3. Ularni timestamp tartibida ishga tushiradi
  4. migrations jadvaliga yozadi — "bu bajarildi"

  Natija:
  query: SELECT * FROM "migrations" ORDER BY "id" DESC
  query: START TRANSACTION
  migration CreateUsersTable1700000000000 ishga tushdi
  query: CREATE TABLE "users" (...)
  query: INSERT INTO "migrations" (name) VALUES (...)
  query: COMMIT
  Migration muvaffaqiyatli bajarildi

migration:revert — orqaga qaytarish

npm run migration:revert
  Faqat ENG SO'NGGI migration qaytariladi.
  Yana qaytarmoqchi bo'lsangiz — yana shu buyruqni bering.

  Nima bo'ladi:
  1. migrations jadvalidagi oxirgi yozuvni topadi
  2. Shu migration faylidagi down() ni ishga tushiradi
  3. migrations jadvalidan o'chiradi

migration:show — holat

npm run migration:show
  Natija:

  [X] CreateUsersTable1700000000000        ← X = bajarilgan
  [X] AddPhoneToUsers1700000001000
  [X] CreatePostsTable1700000002000
  [ ] SplitNameColumn1700000003000         ← bo'sh = bajarilmagan (kutmoqda)

Amaliy misol — to'liq jarayon

Keling, real loyihada migration qanday ishlatilishini ko'ramiz.

Boshlang'ich holat

// user.entity.ts — 1-versiya
@Entity('users')
export class User {
  @PrimaryGeneratedColumn() id: number;
  @Column() name: string;
  @Column({ unique: true }) email: string;
}
# 1. Dastlabki migration yaratamiz
npm run migration:generate -- src/migrations/CreateUsersTable

# 2. Bajaramiz
npm run migration:run
  users jadvali yaratildi:
  ┌────┬──────┬───────┐
  │ id │ name │ email │
  └────┴──────┴───────┘

O'zgarish: telefon raqam qo'shish

// user.entity.ts — 2-versiya
@Column({ nullable: true })
phone: string;     // ← yangi maydon
# Farqni topib migration yaratadi
npm run migration:generate -- src/migrations/AddPhoneToUsers

TypeORM yaratgan fayl:

export class AddPhoneToUsers1700000001000 implements MigrationInterface {

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      ALTER TABLE "users" ADD "phone" character varying
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      ALTER TABLE "users" DROP COLUMN "phone"
    `);
  }
}
npm run migration:run
  users jadvali yangilandi:
  ┌────┬──────┬───────┬───────┐
  │ id │ name │ email │ phone │   ← phone qo'shildi, eski ma'lumotlar saqlanib qoldi
  ├────┼──────┼───────┼───────┤
  │ 1  │ Ali  │ali@.. │ NULL  │
  │ 2  │ Vali │vali@..│ NULL  │
  └────┴──────┴───────┴───────┘

Noto'g'ri migration — orqaga qaytarish

Aytaylik migration noto'g'ri yozildi. Orqaga qaytaramiz:

npm run migration:revert
  AddPhoneToUsers migration bekor qilindi:
  ┌────┬──────┬───────┐
  │ id │ name │ email │   ← phone ustuni o'chirildi
  ├────┼──────┼───────┤
  │ 1  │ Ali  │ali@.. │
  │ 2  │ Vali │vali@..│
  └────┴──────┴───────┘

Migration yozishda qoidalar

1. Har bir migration bitta vazifani bajarsin

// ❌ Yomon — bir migration da ko'p narsa
export class BigMigration implements MigrationInterface {
  async up(queryRunner: QueryRunner) {
    await queryRunner.query(`CREATE TABLE users (...)`);
    await queryRunner.query(`CREATE TABLE posts (...)`);
    await queryRunner.query(`ALTER TABLE users ADD phone ...`);
    await queryRunner.query(`CREATE INDEX ...`);
  }
}

// ✅ Yaxshi — alohida migrationlar
// 1700000000000-CreateUsersTable.ts
// 1700000001000-CreatePostsTable.ts
// 1700000002000-AddPhoneToUsers.ts
// 1700000003000-AddIndexToEmail.ts

2. down() ni har doim to'g'ri yozing

down() ni to'g'ri yozmasangiz — orqaga qaytarish imkoni yo'q bo'ladi. Revert qilgan vaqt muammo chiqadi.

// ❌ down() bo'sh qoldirilgan
async down(queryRunner: QueryRunner) {
  // TODO: implement
}

// ✅ To'g'ri — up() ning teskarisi
async down(queryRunner: QueryRunner) {
  await queryRunner.query(`DROP TABLE "users"`);
}

3. Migration fayllarini hech qachon o'zgartirmang

Migration bajarilgandan keyin uni o'zgartirmang. Agar xato topilsa — yangi migration yozing.

  ❌ Xato:  Bajarilgan migration faylni tahrirlash
            → Baza bilan fayl mos kelmay qoladi
            → Boshqa dasturchilarda xato chiqadi

  ✅ To'g'ri: Yangi migration fayl yarating

4. Git ga commit qiling

git add src/migrations/
git commit -m "feat: add phone column to users table"

Migration fayllar git tarixida saqlanishi shart — boshqa dasturchilarga ham, production serverga ham kerak bo'ladi.


Production da migration

Production da migrationlar ikki xil ishga tushiriladi:

1-usul: migrationsRun: true — avtomatik

// app.module.ts
TypeOrmModule.forRootAsync({
  useFactory: (configService: ConfigService) => ({
    // ...
    migrationsRun: true,
    // Ilova ishga tushganda bajarilmagan migrationlar avtomatik ishga tushadi
    migrations: ['dist/migrations/*{.js}'],
  }),
})

CI/CD pipeline da ilova deploy qilinganda migrationlar avtomatik bajariladi.

2-usul: Deploy skriptida qo'lda

# deploy.sh
echo "Migrationlar ishga tushirilmoqda..."
npm run migration:run

echo "Ilova ishga tushirilmoqda..."
npm run start:prod

💡 Qaysi usul yaxshiroq? Ko'pchilik migrationsRun: true ni tavsiya qiladi — migration va deploy birgalikda bo'ladi, alohida qadam kerak emas.


generate vs create — qachon qaysi?

  migration:generate                    migration:create
  ──────────────────────────────────────────────────────────
  Entity o'zgarganda                  │ Ma'lumotlarni ko'chirish kerak bo'lsa
  Ustun qo'shish / o'chirish          │ Eski ustunni yangi nomga o'tkazish
  Jadval yaratish / o'chirish         │ Bir jadvaldan boshqasiga ma'lumot ko'chirish
  TypeORM o'zi SQL yozadi             │ Index, trigger, view qo'shish
                                      │ Maxsus PostgreSQL SQL kerak bo'lsa

Vazifalar

1-vazifa — Tushunish: synchronize: true va migration ning farqi nima? Quyidagi holatda qaysi birini ishlatish kerak?

  • Uy da o'z kompyuteringizda yangi feature sinab ko'ryapsiz
  • Haqiqiy foydalanuvchilar bor production server

2-vazifa — Amaliy: Post entity ga viewCount (integer, default: 0) ustunini qo'shing. Migration yozib, up() da ustun qo'shilsin, down() da o'chirilsin.

3-vazifa — O'ylash: Aytaylik posts jadvalida 100,000 ta yozuv bor. title ustunini varchar(100) dan varchar(500) ga o'zgartirmoqchisiz. Bu migration production da qancha vaqt olishi mumkin? Xavfli tomonlari bormi?


Xulosa

Migration nima: Ma'lumotlar bazasiga qilinadigan o'zgarishlarni nazorat ostida, qaytarish imkoni bilan amalga oshirish.

Nima uchun kerak: synchronize: true production da ma'lumot yo'qotishi mumkin. Migration esa ma'lumotni saqlab, o'zgarishni xavfsiz qo'llaydi.

Ikki usul:

  • generate — entity o'zgarganda, TypeORM SQL ni o'zi yozadi
  • create — ma'lumot ko'chirish yoki maxsus SQL kerak bo'lganda, qo'lda yoziladi

Asosiy buyruqlar:

  • migration:run — bajarilmagan migrationlarni ishga tushiradi
  • migration:revert — oxirgi migrationni orqaga qaytaradi
  • migration:show — qaysilar bajarilgan, qaysilar kutmoqda

Qoidalar:

  • Har bir migration — bitta vazifa
  • down() ni har doim to'g'ri yozing
  • Bajarilgan migration faylni o'zgartirmang
  • Git ga commit qiling

Keyingi dars

19-dars: TypeORM — Prisma bilan PostgreSQL