Database Relations — One-to-Many, Many-to-Many (Sequelize)

~25 daq · NestJS · Ma'lumotlar bazasi

Muammo

Siz foydalanuvchilar va ularning postlarini saqlayapsiz. Ikkita alohida jadval bor: users va posts. Lekin savol tug'iladi: "Ali ning barcha postlarini qanday olaman?" yoki "Bu post kimniki?"

Agar relationlarni to'g'ri sozlamasangiz, har safar qo'lda WHERE userId = ... yozishga majbur bo'lasiz. Sequelize relationlari bu muammoni hal qiladi — bir marta sozlab, keyin user.getPosts() deb chaqirasiz.

Relation turlari

Ma'lumotlar bazasida jadvallar orasidagi bog'liqlikning uch asosiy turi bor:

One-to-Many (Bir-ko'p): Bir foydalanuvchining ko'p postlari bo'lishi mumkin. Lekin har bir post faqat bitta foydalanuvchiga tegishli. Masalan: User → Posts, Category → Products.

Many-to-Many (Ko'p-ko'p): Bir talaba ko'p kurslarga yozilishi mumkin, va bir kursda ko'p talaba bo'lishi mumkin. Masalan: Students ↔ Courses, Posts ↔ Tags.

One-to-One (Bir-bir): Har bir foydalanuvchining bitta profili bor. Masalan: User ↔ Profile.

Hayotiy o'xshatma

Tasavvur qiling: kutubxona. Bir muallif ko'p kitob yozishi mumkin — bu One-to-Many. Bir kitob ko'p janrga tegishli bo'lishi mumkin, va bir janrda ko'p kitob bo'lishi mumkin — bu Many-to-Many. Kutubxona bu bog'liqliklarni alohida ro'yxatlarda saqlaydi.

Ma'lumotlar bazasida ham xuddi shunday: bog'liqlik foreign key (tashqi kalit) orqali saqlanadi.

Nima uchun shunday?

Nima uchun foreign key ishlatiladi?

Agar posts jadvalida userId maydoni bo'lmasa, qaysi post kimniki ekanini bilish imkonsiz. foreign key — bu bir jadvaldan ikkinchisiga "ko'rsatkich". U ma'lumotlar yaxlitligini ta'minlaydi: mavjud bo'lmagan foydalanuvchiga post qo'shib bo'lmaydi.

Nima uchun Many-to-Many uchun oraliq jadval kerak?

students va courses jadvallarini to'g'ridan-to'g'ri bog'lab bo'lmaydi — chunki bir yacheykaga bir nechta qiymat yozib bo'lmaydi. Shuning uchun student_courses oraliq jadvali yaratiladi: u faqat studentId va courseId ni saqlaydi.

One-to-Many: User → Posts

Modellarni yaratish

// user.model.ts
import { Column, HasMany, Model, Table } from 'sequelize-typescript';
import { Post } from './post.model';

@Table({ tableName: 'users' })
export class User extends Model {
  @Column
  name: string;

  @Column
  email: string;

  @HasMany(() => Post) // ← "Mening ko'p postlarim bor"
  posts: Post[];
}
// post.model.ts
import { BelongsTo, Column, ForeignKey, Model, Table } from 'sequelize-typescript';
import { User } from './user.model';

@Table({ tableName: 'posts' })
export class Post extends Model {
  @Column
  title: string;

  @Column
  content: string;

  @ForeignKey(() => User) // ← Bu maydon User ga bog'liq
  @Column
  userId: number;

  @BelongsTo(() => User) // ← "Men bitta userga tegishliман"
  user: User;
}

@HasMany va @BelongsTo juft bo'lishi shart. Biri "ega", ikkinchisi "tegishli" deydi. Ikkisini ham yozmasangiz, relation to'liq ishlamaydi.

Modulga qo'shish

// posts.module.ts
@Module({
  imports: [
    SequelizeModule.forFeature([Post, User]), // ← Ikkala modelni ham import qiling
  ],
})
export class PostsModule {}

CRUD va relation bilan ishlash

// posts.service.ts
@Injectable()
export class PostsService {
  constructor(
    @InjectModel(Post) private postModel: typeof Post,
    @InjectModel(User) private userModel: typeof User,
  ) {}

  // Post yaratish
  async create(userId: number, title: string, content: string) {
    return this.postModel.create({ userId, title, content }); // ← userId avtomatik bog'laydi
  }

  // Foydalanuvchining barcha postlarini olish
  async findByUser(userId: number) {
    return this.postModel.findAll({
      where: { userId },
    });
  }

  // Post + uning muallifi (JOIN)
  async findOneWithUser(id: number) {
    return this.postModel.findOne({
      where: { id },
      include: [User], // ← JOIN: post bilan birga user ma'lumotini ham oladi
    });
  }

  // Foydalanuvchi + uning barcha postlari
  async findUserWithPosts(userId: number) {
    return this.userModel.findOne({
      where: { id: userId },
      include: [Post], // ← User bilan birga postlarni ham oladi
    });
  }
}

include — bu SQL dagi JOIN. include: [User] deb yozsangiz, Sequelize avtomatik LEFT JOIN users ON posts.userId = users.id so'rovini bajaradi.

Many-to-Many: Students ↔ Courses

Modellar

// student.model.ts
import { BelongsToMany, Column, Model, Table } from 'sequelize-typescript';
import { Course } from './course.model';
import { StudentCourse } from './student-course.model';

@Table({ tableName: 'students' })
export class Student extends Model {
  @Column
  name: string;

  @BelongsToMany(() => Course, () => StudentCourse) // ← "Men ko'p kurslarga tegishliман"
  courses: Course[];
}
// course.model.ts
import { BelongsToMany, Column, Model, Table } from 'sequelize-typescript';
import { Student } from './student.model';
import { StudentCourse } from './student-course.model';

@Table({ tableName: 'courses' })
export class Course extends Model {
  @Column
  title: string;

  @BelongsToMany(() => Student, () => StudentCourse) // ← "Menda ko'p talabalar bor"
  students: Student[];
}
// student-course.model.ts — oraliq jadval
import { Column, ForeignKey, Model, Table } from 'sequelize-typescript';
import { Student } from './student.model';
import { Course } from './course.model';

@Table({ tableName: 'student_courses' })
export class StudentCourse extends Model {
  @ForeignKey(() => Student)
  @Column
  studentId: number;

  @ForeignKey(() => Course)
  @Column
  courseId: number;
}

Many-to-Many bilan ishlash

// students.service.ts
@Injectable()
export class StudentsService {
  constructor(
    @InjectModel(Student) private studentModel: typeof Student,
    @InjectModel(Course) private courseModel: typeof Course,
  ) {}

  // Talabani kursga yozish
  async enroll(studentId: number, courseId: number) {
    const student = await this.studentModel.findByPk(studentId);
    const course = await this.courseModel.findByPk(courseId);

    await student.$add('courses', course); // ← oraliq jadvalga yozuv qo'shadi
  }

  // Talabani kursdan chiqarish
  async unenroll(studentId: number, courseId: number) {
    const student = await this.studentModel.findByPk(studentId);
    await student.$remove('courses', courseId); // ← oraliq jadvaldan o'chiradi
  }

  // Talabaning barcha kurslari
  async getStudentCourses(studentId: number) {
    return this.studentModel.findOne({
      where: { id: studentId },
      include: [Course], // ← JOIN orqali kurslarni ham oladi
    });
  }

  // Kursning barcha talabalari
  async getCourseStudents(courseId: number) {
    return this.courseModel.findOne({
      where: { id: courseId },
      include: [Student],
    });
  }
}

$add, $remove, $set — Sequelize ning Many-to-Many uchun maxsus metodlari. Ular oraliq jadval bilan avtomatik ishlaydi.

One-to-One: User ↔ Profile

// user.model.ts
@HasOne(() => Profile) // ← "Mening bitta profilim bor"
profile: Profile;

// profile.model.ts
@ForeignKey(() => User)
@Column
userId: number;

@BelongsTo(() => User)
user: User;

@HasOne va @HasMany ning farqi faqat bitta: @HasOne faqat bitta bog'liq yozuvga ruxsat beradi.

Tez-tez uchraydigan xatolar

1. include ishlamaydi — bo'sh array qaytadi

Sabab: Model SequelizeModule.forFeature([...]) ga qo'shilmagan. Har bir modul o'zi ishlatadigan barcha modellarni import qilishi shart.

// ✅ To'g'ri
SequelizeModule.forFeature([Post, User]) // ← User ni ham qo'shing

2. @HasMany yozildi, lekin @BelongsTo yozilmadi

Sabab: Relation bir tomonlama bo'lib qoladi. user.getPosts() ishlaydi, lekin post.getUser() ishlamaydi. Har doim juft yozing.

3. Many-to-Many da $add xato beradi

Sabab: findByPk null qaytargan — ya'ni bunday ID li yozuv yo'q. $add ni chaqirishdan oldin tekshiring:

if (!student || !course) throw new NotFoundException();
await student.$add('courses', course);

4. Circular import xatosi

Sabab: User modeli Post ni import qiladi, Post modeli User ni import qiladi — bu circular dependency. Yechim: () => Post kabi lazy reference ishlatiladi (Sequelize dekoratorlarida bu allaqachon shunday).

5. include da chuqur nested relation

// ✅ Nested include — post → user → user ning profili
include: [{
  model: User,
  include: [Profile],
}]

Xulosa

  • One-to-Many: @HasMany + @BelongsTo + @ForeignKey — eng ko'p ishlatiladigan relation
  • Many-to-Many: @BelongsToMany + oraliq jadval — $add, $remove metodlari bilan boshqariladi
  • One-to-One: @HasOne + @BelongsTo — profil, sozlamalar kabi yagona bog'liqliklar uchun
  • include — SQL JOIN ning Sequelize ko'rinishi, relation ma'lumotlarini birga olish uchun
  • Har doim ikkala tomonni ham yozing: @HasMany bo'lsa, @BelongsTo ham bo'lsin

Keyingi dars: 15-dars: Database Migrations (Sequelize)