Database Relations (TypeORM)

~25 daq · NestJS · Ma'lumotlar bazasi

Muammodan boshlaylik

Foydalanuvchi ro'yxatdan o'tdi. Uning profil ma'lumotlari bor — bio, avatar, telefon raqam. U postlar yozadi. Har bir postda teglar (tag) bor.

Bularni qanday saqlash kerak? Hammani bitta jadvalga joylashtirish mumkinmi?

❌ Yomon usul — bitta katta jadval:

| id | ism  | bio        | post1_sarlavha | post1_matn | post2_sarlavha | tag1  | tag2   |
|----|------|------------|----------------|------------|----------------|-------|--------|
| 1  | Ali  | Dasturchi  | Salom dunyo    | ...        | TypeORM haqida | nestjs| typeorm|
| 2  | Vali | Dizayner   | NULL           | NULL       | NULL           | NULL  | NULL   |

Bu yondashuv bilan muammolar:

  • Ali 10 ta post yozsa — 10 ta ustun kerakmi?
  • Bitta postda 20 ta tag bo'lsa — 20 ta ustun?
  • Vali hech post yozmagan — jadvalda bo'sh (NULL) kataklar to'lib ketadi

Yechim — ma'lumotlarni alohida jadvallarga ajratish va ular orasida bog'liqlik (relation) o'rnatish.


Bog'liqlik nima?

Bog'liqlik — ikki jadval orasidagi aloqa. Bir jadvaldagi ma'lumot boshqa jadvaldagi ma'lumotga "ishora" qiladi.

✅ To'g'ri usul — alohida jadvallar:

  users jadvali            posts jadvali
  ┌────┬──────┐            ┌────┬─────────────────┬─────────┐
  │ id │ ism  │            │ id │ sarlavha         │ user_id │
  ├────┼──────┤            ├────┼─────────────────┼─────────┤
  │ 1  │ Ali  │◄───────────│ 1  │ Salom dunyo      │    1    │
  │ 2  │ Vali │            │ 2  │ TypeORM haqida   │    1    │
  └────┴──────┘            │ 3  │ CSS tricks       │    2    │
                           └────┴─────────────────┴─────────┘
                                        │
                             user_id = 1 degani:
                             "bu post Ali nikiga tegishli"

posts jadvalidagi user_id ustuni — foreign key (tashqi kalit). U users jadvalidagi id ga ishora qiladi. Shu orqali "qaysi post kimniki" ma'lum bo'ladi.


Bog'liqlik turlari

Uchta asosiy tur bor:

  One-to-One    │  One-to-Many   │  Many-to-Many
  ─────────────────────────────────────────────
  User ──── Profile  │  User ──< Posts  │  Posts >──< Tags
                     │                  │
  1 foydalanuvchi    │  1 foydalanuvchi  │  1 post ko'p
  = 1 profil         │  = ko'p postlar  │  tegga ega bo'lishi
                     │                  │  mumkin va 1 teg
  1 profil           │  1 post faqat    │  ko'p postda
  = 1 foydalanuvchi  │  1 foydalanuvchiga│  ishlatilishi mumkin

Har birini alohida ko'ramiz.


1. One-to-One — birga bir

Qoidasi: A jadvalning har bir yozuvi B jadvalning faqat bitta yozuvi bilan bog'liq. Va aksincha.

Misol: Har bir foydalanuvchining bitta profili bor. Har bir profil faqat bitta foydalanuvchiga tegishli.

  users jadvali            profiles jadvali
  ┌────┬───────┬────────┐  ┌────┬──────────┬───────┬─────────┐
  │ id │ name  │ email  │  │ id │ bio      │ phone │ user_id │
  ├────┼───────┼────────┤  ├────┼──────────┼───────┼─────────┤
  │ 1  │ Ali   │ ali@.. │◄─│ 1  │Dasturchi │998901 │    1    │
  │ 2  │ Vali  │vali@.. │◄─│ 2  │Dizayner  │998902 │    2    │
  └────┴───────┴────────┘  └────┴──────────┴───────┴─────────┘

  profiles.user_id → users.id
  (foreign key profiles jadvalida yotadi)

Entity lar:

// users/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm';
import { Profile } from '../../profiles/entities/profile.entity';

@Entity('users')
export class User {

  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

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

  @OneToOne(() => Profile, (profile) => profile.user, {
    cascade: true,   // user saqlanganda profile ham saqlansin
    eager: false,    // user olinganda profile avtomatik kelmaydi
  })
  profile: Profile;
}
// profiles/entities/profile.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { User } from '../../users/entities/user.entity';

@Entity('profiles')
export class Profile {

  @PrimaryGeneratedColumn()
  id: number;

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

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

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

  @OneToOne(() => User, (user) => user.profile)
  @JoinColumn()   // ← shu dekorator foreign key qaysi jadvalda bo'lishini belgilaydi
  user: User;
}

@JoinColumn() nima qiladi?

@JoinColumn() — foreign key (user_id ustuni) shu jadvalda bo'lishini aytadi. Uni faqat bir tomonga — bog'liqlikni "saqlovchi" tomonga yoziladi.

  @JoinColumn() profiles da yozildi
          ↓
  profiles jadvalida user_id ustuni paydo bo'ladi
  users jadvalida hech qanday qo'shimcha ustun bo'lmaydi

Ishlatish — service da:

// Profile bilan birga user yaratish
async create(createUserDto: CreateUserDto) {
  const user = this.userRepository.create({
    name: createUserDto.name,
    email: createUserDto.email,
    profile: {
      bio: createUserDto.bio,
      phone: createUserDto.phone,
    },
    // cascade: true bo'lgani uchun profile ham avtomatik saqlanadi
  });

  return await this.userRepository.save(user);
}

// User ni profili bilan birga olish
async findOneWithProfile(id: number) {
  return await this.userRepository.findOne({
    where: { id },
    relations: { profile: true },
    // relations — qo'shimcha JOIN qilib profil ham olinsin
  });
}

Natija:

{
  "id": 1,
  "name": "Ali",
  "email": "ali@example.com",
  "profile": {
    "id": 1,
    "bio": "Dasturchi",
    "phone": "998901234567",
    "avatar": null
  }
}

2. One-to-Many / Many-to-One — birga ko'p

Qoidasi: A jadvalning bitta yozuvi B jadvalning ko'p yozuvlari bilan bog'liq. Lekin B jadvalning har bir yozuvi faqat A ning bitta yozuviga tegishli.

Misol: Bir foydalanuvchi ko'p post yozishi mumkin. Lekin har bir post faqat bitta foydalanuvchiga tegishli.

  users jadvali          posts jadvali
  ┌────┬───────┐         ┌────┬──────────────────┬─────────┐
  │ id │ name  │         │ id │ title            │ user_id │
  ├────┼───────┤         ├────┼──────────────────┼─────────┤
  │    │       │◄────────│ 1  │ Salom dunyo       │    1    │
  │ 1  │ Ali   │◄────────│ 2  │ NestJS haqida    │    1    │
  │    │       │◄────────│ 3  │ TypeORM — CRUD   │    1    │
  ├────┼───────┤         ├────┼──────────────────┼─────────┤
  │ 2  │ Vali  │◄────────│ 4  │ CSS qo'llanma    │    2    │
  └────┴───────┘         └────┴──────────────────┴─────────┘

  Ali = 3 ta post     Vali = 1 ta post
  foreign key (user_id) DOIM "ko'p" tomonda yotadi — ya'ni posts da

Bu ikki tomoni bor:

  • User → Post tomoni: One-to-Many (bir user — ko'p post)
  • Post → User tomoni: Many-to-One (ko'p post — bir user)

Entity lar:

// users/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Post } from '../../posts/entities/post.entity';

@Entity('users')
export class User {

  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @OneToMany(() => Post, (post) => post.user)
  // "Men bir — Post lar ko'p. Post lardagi 'user' maydoni menga qaraydi."
  posts: Post[];
}
// posts/entities/post.entity.ts
import {
  Entity, PrimaryGeneratedColumn, Column,
  ManyToOne, JoinColumn, CreateDateColumn
} from 'typeorm';
import { User } from '../../users/entities/user.entity';

@Entity('posts')
export class Post {

  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column('text')
  content: string;

  @CreateDateColumn()
  createdAt: Date;

  @ManyToOne(() => User, (user) => user.posts, {
    onDelete: 'CASCADE',
    // User o'chirilsa — uning postlari ham o'chsin
    // onDelete: 'SET NULL' — user_id ni NULL ga qo'ysin (nullable: true kerak)
    // onDelete: 'RESTRICT' — user ni o'chirsa xato bering (standart)
  })
  // @JoinColumn() shart emas — Many tomonda foreign key avtomatik paydo bo'ladi
  user: User;
}

@JoinColumn() kerakmi? One-to-Many / Many-to-One da @JoinColumn() yozmaslik ham bo'ladi — TypeORM ManyToOne tomoniga avtomatik user_id ustunini qo'shadi. Lekin yozsa ham ishlaydi.

Ishlatish — service da:

// Post yaratish
async createPost(userId: number, createPostDto: CreatePostDto) {
  const user = await this.userRepository.findOne({ where: { id: userId } });

  if (!user) throw new NotFoundException('Foydalanuvchi topilmadi');

  const post = this.postRepository.create({
    ...createPostDto,
    user,   // ← bog'liqlik shu yerda o'rnatiladi
  });

  return await this.postRepository.save(post);
  // SQL: INSERT INTO posts (title, content, user_id) VALUES (?, ?, ?)
}

// Foydalanuvchining barcha postlarini olish
async findUserPosts(userId: number) {
  return await this.postRepository.find({
    where: { user: { id: userId } },
    order: { createdAt: 'DESC' },
  });
}

// User ni postlari bilan birga olish
async findUserWithPosts(id: number) {
  return await this.userRepository.findOne({
    where: { id },
    relations: { posts: true },
  });
}

Natija:

{
  "id": 1,
  "name": "Ali",
  "posts": [
    { "id": 1, "title": "Salom dunyo", "createdAt": "2026-01-01" },
    { "id": 2, "title": "NestJS haqida", "createdAt": "2026-01-02" },
    { "id": 3, "title": "TypeORM — CRUD", "createdAt": "2026-01-03" }
  ]
}

3. Many-to-Many — ko'pga ko'p

Qoidasi: A jadvalning ko'p yozuvlari B jadvalning ko'p yozuvlari bilan bog'liq bo'lishi mumkin.

Misol: Bitta postda bir nechta teg bo'lishi mumkin. Va bitta teg bir nechta postda ishlatilishi mumkin.

  posts jadvali       (oraliq jadval)        tags jadvali
  ┌────┬──────────┐   ┌─────────┬────────┐   ┌────┬─────────┐
  │ id │ title    │   │ post_id │ tag_id │   │ id │  name   │
  ├────┼──────────┤   ├─────────┼────────┤   ├────┼─────────┤
  │ 1  │ NestJS   │──►│    1    │   1    │◄──│ 1  │ nestjs  │
  │    │          │──►│    1    │   2    │◄──│ 2  │typeorm  │
  │    │          │   │    1    │   3    │◄──│ 3  │postgres │
  │ 2  │ CSS tips │──►│    2    │   4    │◄──│ 4  │   css   │
  │    │          │──►│    2    │   1    │   └────┴─────────┘
  └────┴──────────┘   └─────────┴────────┘

  Post 1 (NestJS): nestjs, typeorm, postgres teglari bor
  Post 2 (CSS tips): css, nestjs teglari bor
  "nestjs" tegi 2 ta postda ishlatilgan

Ko'rinib turibdiki, Many-to-Many bog'liqlikda oraliq jadval paydo bo'ladi. Bu jadval ikki tomonning ID larini saqlaydi. TypeORM bu jadvalni avtomatik yaratadi — siz yaratishingiz shart emas.

Entity lar:

// posts/entities/post.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Tag } from '../../tags/entities/tag.entity';

@Entity('posts')
export class Post {

  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column('text')
  content: string;

  @ManyToMany(() => Tag, (tag) => tag.posts, { cascade: true })
  @JoinTable()
  // @JoinTable() — oraliq jadvalni qaysi entity boshqarishini belgilaydi
  // Faqat bir tomonga yoziladi — odatda "asosiy" tomonga (bu yerda Post)
  tags: Tag[];
}
// tags/entities/tag.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
import { Post } from '../../posts/entities/post.entity';

@Entity('tags')
export class Tag {

  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  name: string;

  @ManyToMany(() => Post, (post) => post.tags)
  // @JoinTable() bu tomonda yo'q — Post tomonida bor
  posts: Post[];
}

Ishlatish — service da:

// Post teglar bilan yaratish
async createPost(createPostDto: CreatePostDto) {
  const { tagIds, ...postData } = createPostDto;

  // Teglarni bazadan topamiz
  const tags = await this.tagRepository.findBy({
    id: In(tagIds),   // In([1, 2, 3]) — id si 1, 2 yoki 3 ga teng
  });

  const post = this.postRepository.create({
    ...postData,
    tags,   // ← teglar ro'yxati shu yerda biriktiriladi
  });

  return await this.postRepository.save(post);
  // TypeORM o'zi oraliq jadvalga yozadi:
  // INSERT INTO post_tags_tag (post_id, tag_id) VALUES (1,1), (1,2), (1,3)
}

// Post ni teglari bilan olish
async findPostWithTags(id: number) {
  return await this.postRepository.findOne({
    where: { id },
    relations: { tags: true },
  });
}

// Postga teg qo'shish
async addTag(postId: number, tagId: number) {
  const post = await this.postRepository.findOne({
    where: { id: postId },
    relations: { tags: true },
  });

  const tag = await this.tagRepository.findOneBy({ id: tagId });

  post.tags = [...post.tags, tag];
  return await this.postRepository.save(post);
}

// Postdan teg olib tashlash
async removeTag(postId: number, tagId: number) {
  const post = await this.postRepository.findOne({
    where: { id: postId },
    relations: { tags: true },
  });

  post.tags = post.tags.filter(tag => tag.id !== tagId);
  return await this.postRepository.save(post);
}

relations — qanday yuklanadi?

TypeORM da bog'liq ma'lumotlar avtomatik kelmaydi. Siz so'ramasangiz — faqat asosiy jadval ma'lumotlari keladi.

// Faqat user — postlarsiz
const user = await userRepository.findOne({ where: { id: 1 } });
// { id: 1, name: "Ali", email: "..." }
// posts: undefined ← kelmadi!

// User postlari bilan
const user = await userRepository.findOne({
  where: { id: 1 },
  relations: { posts: true },
  // SQL: SELECT * FROM users LEFT JOIN posts ON posts.user_id = users.id
});
// { id: 1, name: "Ali", posts: [...] }

// Ichma-ich relations — postlarning teglari ham kerak bo'lsa
const user = await userRepository.findOne({
  where: { id: 1 },
  relations: {
    posts: {
      tags: true,   // posts ni olib, ichidagi tags ni ham ol
    },
  },
});

Nima uchun avtomatik kelmaydi? Tasavvur qiling: foydalanuvchini olishda u yozgan barcha postlar, har bir postning teglari, kommentariyalar... hammasi avtomatik yuklanib ketsa — bitta so'rovda yuzlab jadvaldan ma'lumot tortib olinadi. Bu ishlashni keskin sekinlashtiradi. Shuning uchun TypeORM "kerak bo'lsa o'zing so'ra" prinsipi bilan ishlaydi.


eager — har doim birga yuklansin

Agar relation har doim birga kelishi kerak bo'lsa — eager: true yoziladi:

@OneToMany(() => Post, (post) => post.user, {
  eager: true,   // user olinganda postlari har doim birga keladi
})
posts: Post[];
// eager: true bo'lganda relations yozmasdan ham postlar keladi
const user = await userRepository.findOne({ where: { id: 1 } });
// { id: 1, name: "Ali", posts: [...] }  ← postlar avtomatik keldi

⚠️ eager: true ehtiyotkorlik bilan ishlatilsin. Foydalanuvchilar ro'yxatini olsangiz (findAll) — har bir foydalanuvchining barcha postlari ham yuklanadi. 100 ta foydalanuvchi × 50 ta post = 5000 ta yozuv bir so'rovda. Bu jiddiy muammo bo'lishi mumkin.


cascade — birga saqla, birga o'chir

cascade — bog'liq yozuvlar bilan nima qilish kerakligini belgilaydi.

@OneToMany(() => Post, (post) => post.user, {
  cascade: true,
  // cascade: true degani — ['insert', 'update', 'remove', 'soft-remove', 'recover'] hammasi
  // cascade: ['insert', 'update'] — faqat qo'shish va yangilash
})
posts: Post[];

Amalda nima qiladi:

// cascade: true — user bilan birga post ham saqlanadi
const user = this.userRepository.create({
  name: 'Ali',
  email: 'ali@example.com',
  posts: [
    { title: 'Birinchi post', content: '...' },
    { title: 'Ikkinchi post', content: '...' },
  ],
});

await this.userRepository.save(user);
// 1. users jadvaliga yoziladi
// 2. posts jadvaliga ham yoziladi — postRepository.save() shart emas

onDelete — bog'liq yozuv o'chirilganda nima bo'lishi:

@ManyToOne(() => User, (user) => user.posts, {
  onDelete: 'CASCADE',   // user o'chirilsa post ham o'chsin
  // onDelete: 'SET NULL' — user_id = NULL bo'lsin (nullable: true kerak)
  // onDelete: 'RESTRICT' — user o'chirib bo'lmaydi, avval postlarni o'chir
})
user: User;

Uchala bog'liqlikni taqqoslash

  ┌──────────────┬──────────────────────┬────────────────────────┬────────────────────────┐
  │              │    One-to-One        │    One-to-Many         │    Many-to-Many        │
  ├──────────────┼──────────────────────┼────────────────────────┼────────────────────────┤
  │ Misol        │ User ↔ Profile       │ User → Posts           │ Posts ↔ Tags           │
  ├──────────────┼──────────────────────┼────────────────────────┼────────────────────────┤
  │ Foreign key  │ Bir tomonda          │ "Ko'p" tomonda         │ Oraliq jadvalda        │
  │ qayerda?     │ (@JoinColumn da)     │ (posts.user_id)        │ (post_tags_tag)        │
  ├──────────────┼──────────────────────┼────────────────────────┼────────────────────────┤
  │ Dekoratorlar │ @OneToOne            │ @OneToMany             │ @ManyToMany            │
  │              │ @JoinColumn          │ @ManyToOne             │ @JoinTable             │
  ├──────────────┼──────────────────────┼────────────────────────┼────────────────────────┤
  │ Oraliq jadval│ Yo'q                 │ Yo'q                   │ Ha — avtomatik         │
  └──────────────┴──────────────────────┴────────────────────────┴────────────────────────┘

Loyiha tuzilishi

src/
├── users/
│   ├── entities/user.entity.ts      ← @OneToOne(Profile), @OneToMany(Post)
│   └── users.module.ts              ← forFeature([User])
├── profiles/
│   ├── entities/profile.entity.ts   ← @OneToOne(User), @JoinColumn()
│   └── profiles.module.ts           ← forFeature([Profile])
├── posts/
│   ├── entities/post.entity.ts      ← @ManyToOne(User), @ManyToMany(Tag), @JoinTable()
│   └── posts.module.ts              ← forFeature([Post])
└── tags/
    ├── entities/tag.entity.ts       ← @ManyToMany(Post)
    └── tags.module.ts               ← forFeature([Tag])

Vazifalar

1-vazifa — Tushunish: Quyidagi holatda qaysi bog'liqlik turi ishlatiladi va foreign key qaysi jadvalda bo'ladi?

  • Shahar va mamlakat (bir mamlakatda ko'p shahar)
  • Talaba va kurs (bir talaba ko'p kursga yozilishi mumkin, bir kursda ko'p talaba)
  • Kompaniya va CEO (bir kompaniyaning bitta bosh direktori)

2-vazifa — Amaliy: Category entity yarating. Post bilan One-to-Many bog'liqlik o'rnating: bir kategoriyada ko'p post bo'lishi mumkin, lekin har bir post faqat bitta kategoriyaga tegishli.

3-vazifa — O'ylash: eager: true ni har doim yoqib qo'ysak nima muammo bo'ladi? Qachon eager: true, qachon relations: { ... } ishlatish to'g'riroq?


Xulosa

One-to-One — bitta yozuv bitta yozuv bilan. @JoinColumn() foreign key qayerda bo'lishini belgilaydi.

One-to-Many / Many-to-One — bitta yozuv ko'p yozuv bilan. Foreign key doim "ko'p" tomonda (posts.user_id). @OneToMany va @ManyToOne juft ishlatiladi.

Many-to-Many — ko'p yozuv ko'p yozuv bilan. TypeORM oraliq jadvalni avtomatik yaratadi. @JoinTable() faqat bir tomonga yoziladi.

relations — bog'liq ma'lumotlar avtomatik kelmaydi, relations: { posts: true } bilan so'raladi.

cascade — bog'liq yozuvlarni birga saqlash/o'chirish uchun.

onDelete — bog'liq yozuv o'chirilganda nima bo'lishi: CASCADE, SET NULL, RESTRICT.


Keyingi dars

18-dars: TypeORM — Migration