TypeORM — entity, repository, relation
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'ektdaidbo'lsaUPDATE, bo'lmasaINSERT. LekinUPDATEdan oldin u ko'pinchaSELECTham qiladi — ommaviy o'zgarishlar uchunupdate()yokiinsert()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 —
@Entityclass; 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 —wheremassivirelationssiz munosabatlar yuklanmaydi- QueryBuilder — murakkab so'rovlar,
:paramplaceholder lar transaction(async manager => ...);lock: pessimistic_write=FOR UPDATEsynchronize: false; migratsiyalarni generatsiyadan keyin o'qing
Keyingi dars
60-dars: Real loyiha — to'liq DB schema loyihalash va yozish