TypeORM — entity, repository, relation

~25 daq · MySQL · MySQL va Dasturlash Tillari

Muammodan boshlaylik

NestJS loyihasida ishlayapsiz. NestJS — classlar, decorator lar (@Controller, @Injectable) va dependency injection ga asoslangan. Ma'lumotlar qatlami ham shu uslubda bo'lsa — kod yaxlit ko'rinadi.

TypeORM — aynan shunday ORM: jadvallar TypeScript classlari va decorator lar bilan tasvirlanadi. NestJS rasmiy hujjatlarida asosiy ORM sifatida ko'rsatiladi. Java (Hibernate) yoki C# (Entity Framework) dan kelganlar uchun juda tanish.


O'rnatish

npm install typeorm mysql2 reflect-metadata

tsconfig.json da decorator lar yoqilgan bo'lishi kerak:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

data-source.ts:

import 'reflect-metadata';
import { DataSource } from 'typeorm';

export const AppDataSource = new DataSource({
  type: 'mysql',
  host: process.env.DB_HOST,
  port: 3306,
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: 'dokon',
  charset: 'utf8mb4',
  entities: [__dirname + '/entities/*.{ts,js}'],
  migrations: [__dirname + '/migrations/*.{ts,js}'],
  synchronize: false,      // ⚠️ production da doim false
  logging: ['query', 'error'],
});

await AppDataSource.initialize();

[!WARNING] synchronize: true — TypeORM har ishga tushganda jadvallarni entity larga moslab avtomatik o'zgartiradi. Ustun nomini o'zgartirsangiz — u eski ustunni o'chiradi va yangisini yaratadi. Ma'lumot yo'qoladi. Faqat tajriba uchun; real loyihada — migratsiyalar.


Entity — jadval class sifatida

entities/Mijoz.ts:

import {
  Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn,
  OneToMany, Index,
} from 'typeorm';
import { Buyurtma } from './Buyurtma';

@Entity('mijozlar')
export class Mijoz {
  @PrimaryGeneratedColumn({ unsigned: true })
  id: number;

  @Column({ length: 100 })
  ism: string;

  @Index({ unique: true })
  @Column({ length: 255 })
  email: string;

  @Column({ type: 'varchar', length: 20, nullable: true })
  telefon: string | null;

  @Column({ type: 'varchar', length: 50, nullable: true })
  shahar: string | null;

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date;

  @OneToMany(() => Buyurtma, (b) => b.mijoz)
  buyurtmalar: Buyurtma[];
}

entities/Buyurtma.ts:

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn, CreateDateColumn, Index } from 'typeorm';
import { Mijoz } from './Mijoz';
import { BuyurtmaQatori } from './BuyurtmaQatori';

export enum BuyurtmaHolati {
  Yangi = 'yangi',
  Tolangan = 'tolangan',
  Yolda = 'yolda',
  Yetkazildi = 'yetkazildi',
  Bekor = 'bekor',
}

@Entity('buyurtmalar')
@Index(['mijozId', 'createdAt'])
export class Buyurtma {
  @PrimaryGeneratedColumn({ unsigned: true })
  id: number;

  @Column({ name: 'mijoz_id', unsigned: true })
  mijozId: number;

  @ManyToOne(() => Mijoz, (m) => m.buyurtmalar, { onDelete: 'RESTRICT' })
  @JoinColumn({ name: 'mijoz_id' })
  mijoz: Mijoz;

  @Column({ type: 'enum', enum: BuyurtmaHolati, default: BuyurtmaHolati.Yangi })
  holat: BuyurtmaHolati;

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date;

  @OneToMany(() => BuyurtmaQatori, (q) => q.buyurtma, { cascade: ['insert'] })
  qatorlar: BuyurtmaQatori[];
}

Decorator lar:

Decorator Ma'nosi
@Entity('jadval') Class — jadval
@PrimaryGeneratedColumn() AUTO_INCREMENT PRIMARY KEY
@Column({...}) Ustun: length, nullable, default, type, name
@CreateDateColumn, @UpdateDateColumn Avtomatik vaqt belgilari
@Index() Indeks; class ustida — composite
@ManyToOne + @JoinColumn Foreign key shu jadvalda
@OneToMany Teskari tomon (bazada ustun yo'q)

Munosabatlar

// One-to-Many / Many-to-One (18-dars)
@ManyToOne(() => Toifa, (t) => t.mahsulotlar, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'toifa_id' })
toifa: Toifa | null;

// One-to-One (20-dars)
@OneToOne(() => Profil, (p) => p.foydalanuvchi)
profil: Profil;

// Many-to-Many (19-dars)
@ManyToMany(() => Teg, (t) => t.mahsulotlar)
@JoinTable({
  name: 'mahsulot_teglar',
  joinColumn: { name: 'mahsulot_id' },
  inverseJoinColumn: { name: 'teg_id' },
})
teglar: Teg[];

Repository — CRUD

import { AppDataSource } from './data-source';
import { Mijoz } from './entities/Mijoz';
import { Mahsulot } from './entities/Mahsulot';
import { Between, Like, MoreThan, In } from 'typeorm';

const mijozRepo = AppDataSource.getRepository(Mijoz);
const mahsulotRepo = AppDataSource.getRepository(Mahsulot);

// CREATE
const m = mijozRepo.create({ ism: 'Dilshod', email: 'dilshod@mail.uz', shahar: 'Toshkent' });
await mijozRepo.save(m);
console.log(m.id);

// READ
const bitta = await mijozRepo.findOneBy({ id: 1 });
const email = await mijozRepo.findOne({ where: { email: 'aziz@mail.uz' } });

const mahsulotlar = await mahsulotRepo.find({
  where: {
    narx: Between(100000, 5000000),
    soni: MoreThan(0),
    nom: Like('%Pro%'),
  },
  select: { id: true, nom: true, narx: true },
  order: { narx: 'ASC' },
  take: 20,
  skip: 0,
});

// OR shartlar — massiv
await mahsulotRepo.find({ where: [{ toifaId: 1 }, { toifaId: 3 }] });

// UPDATE
await mahsulotRepo.update({ id: 5 }, { narx: 2990000 });
await mahsulotRepo.decrement({ id: 5 }, 'soni', 1);     // atomar: soni = soni - 1

// DELETE
await mijozRepo.delete({ id: 4 });

// Hisoblash
const soni = await mijozRepo.countBy({ shahar: 'Toshkent' });

[!INFO] save() — ob'ektda id bo'lsa UPDATE, bo'lmasa INSERT. Lekin UPDATE dan oldin u ko'pincha SELECT ham qiladi — ommaviy o'zgarishlar uchun update() yoki insert() tezroq.

Munosabatlarni yuklash

const buyurtma = await AppDataSource.getRepository(Buyurtma).findOne({
  where: { id: 3 },
  relations: { mijoz: true, qatorlar: { mahsulot: true } },
});

console.log(buyurtma?.mijoz.ism);
console.log(buyurtma?.qatorlar[0].mahsulot.nom);

relations siz buyurtma.mijoz — undefined (lazy loading standart holatda yo'q). Bu N+1 dan himoya qiladi, lekin "nega undefined?" degan savolning eng ko'p sababi.


QueryBuilder — murakkab so'rovlar

find() imkoniyatlari yetmaganda — SQL ga yaqin, lekin tiplangan quruvchi:

const natija = await AppDataSource
  .getRepository(Mijoz)
  .createQueryBuilder('m')
  .select('m.ism', 'ism')
  .addSelect('COUNT(DISTINCT b.id)', 'buyurtmalar')
  .addSelect('COALESCE(SUM(q.soni * q.narx), 0)', 'jami')
  .leftJoin('m.buyurtmalar', 'b', 'b.holat != :bekor', { bekor: 'bekor' })
  .leftJoin('b.qatorlar', 'q')
  .where('m.shahar = :shahar', { shahar: 'Toshkent' })
  .groupBy('m.id')
  .orderBy('jami', 'DESC')
  .getRawMany();

:shahar va { shahar: ... } — nomlangan placeholder, xavfsiz. getRawMany() — xom natija (agregatlar uchun), getMany() — entity lar.


Tranzaksiyalar

const buyurtmaId = await AppDataSource.transaction(async (manager) => {
  const mahsulot = await manager.findOne(Mahsulot, {
    where: { id: 5 },
    lock: { mode: 'pessimistic_write' },          // SELECT ... FOR UPDATE (38-dars)
  });
  if (!mahsulot || mahsulot.soni < 1) throw new Error("Omborda yo'q");

  await manager.decrement(Mahsulot, { id: 5 }, 'soni', 1);

  const b = manager.create(Buyurtma, {
    mijozId: 2,
    qatorlar: [{ mahsulotId: 5, soni: 1, narx: mahsulot.narx }],
  });
  await manager.save(b);           // cascade: ['insert'] — qatorlar ham saqlanadi
  return b.id;
});

Tranzaksiya ichida faqat manager orqali ishlang — getRepository() yoki global AppDataSource dan foydalansangiz, so'rov tranzaksiyadan tashqariga chiqadi.


Migratsiyalar

# Entity lar va bazani solishtirib, migratsiya yaratish
npx typeorm-ts-node-commonjs migration:generate src/migrations/AddTugilganKun -d src/data-source.ts

# Qo'llash va qaytarish
npx typeorm-ts-node-commonjs migration:run -d src/data-source.ts
npx typeorm-ts-node-commonjs migration:revert -d src/data-source.ts

Yaratilgan fayl:

export class AddTugilganKun1730700000000 implements MigrationInterface {
  async up(q: QueryRunner) {
    await q.query('ALTER TABLE `mijozlar` ADD `tugilgan_kun` date NULL');
  }
  async down(q: QueryRunner) {
    await q.query('ALTER TABLE `mijozlar` DROP COLUMN `tugilgan_kun`');
  }
}

migration:generate dan keyin SQL ni albatta o'qing — ustun nomini o'zgartirishni TypeORM ko'pincha "o'chirish + yaratish" deb tushunadi va ma'lumot yo'qoladi. Bunday holatda migratsiyani qo'lda RENAME COLUMN ga tuzating (8-dars).


NestJS bilan

// app.module.ts
TypeOrmModule.forRoot({ type: 'mysql', /* ... */ autoLoadEntities: true, synchronize: false }),

// mijozlar.module.ts
TypeOrmModule.forFeature([Mijoz]),

// mijozlar.service.ts
@Injectable()
export class MijozlarService {
  constructor(@InjectRepository(Mijoz) private readonly repo: Repository<Mijoz>) {}

  topEmail(email: string) {
    return this.repo.findOneBy({ email });
  }
}

Repository dependency injection orqali keladi — testlarda soxta (mock) repository bilan almashtirish oson.


Uch ORM — qisqa solishtirish

Sequelize Prisma TypeORM
Sxema JS modellar schema.prisma TS classlar + decorator lar
TypeScript Qisman Eng kuchli (generatsiya) Yaxshi
Uslub Active Record Funksional mijoz Data Mapper (repository) + Active Record
NestJS Bor Yaxshi Rasmiy, eng ko'p
Murakkab so'rovlar sequelize.query $queryRaw QueryBuilder — kuchli

Hammasi bir xil MySQL ustida ishlaydi — va ularning hammasini tushunish uchun kursning oldingi 56 ta darsi kerak edi.


Nima uchun shunday?

Nima uchun TypeORM "Data Mapper" uslubini taklif qiladi — entity ning o'zi save() qilsa (Active Record) oddiyroq emasmi?

Active Record da entity — ham ma'lumot, ham baza bilan ishlash mantiqi. Bu kichik loyihada qulay. Lekin katta loyihada entity "biladi" qanday saqlanishini — uni bazasiz test qilish, boshqa manbaga o'tkazish qiyinlashadi. Data Mapper da entity — oddiy ma'lumot classi, saqlash esa alohida repository ning ishi. Bu mas'uliyatlarni ajratadi: biznes mantiq entity bilan, ma'lumotlar bazasi — repository bilan. NestJS ning dependency injection arxitekturasi aynan shu ajratishga mos keladi.


Xulosa

Eslab qoling:

  • Entity — @Entity class; ustunlar — @Column, @PrimaryGeneratedColumn
  • name: 'snake_case' — bazadagi nom; kodda camelCase
  • @ManyToOne + @JoinColumn, @OneToMany, @OneToOne, @ManyToMany + @JoinTable
  • Repository: find, findOneBy, save, update, delete, decrement
  • Between, Like, MoreThan; OR — where massivi
  • relations siz munosabatlar yuklanmaydi
  • QueryBuilder — murakkab so'rovlar, :param placeholder lar
  • transaction(async manager => ...); lock: pessimistic_write = FOR UPDATE
  • synchronize: false; migratsiyalarni generatsiyadan keyin o'qing

Keyingi dars

60-dars: Real loyiha — to'liq DB schema loyihalash va yozish