Database Relations (Mongoose)

~25 daq · NestJS · Ma'lumotlar bazasi

Muammodan boshlaylik

Oldingi darsda bitta users collection bilan ishladik. Haqiqiy loyihada esa ko'proq:

  • Foydalanuvchining postlari bor
  • Postning izohlari bor
  • Har bir izoh biror foydalanuvchiga tegishli

Bularni qanday bog'laymiz? Mongoose da buning uchun ref va populate bor.


Munosabat turlari

One-to-Many (bir — ko'p):
  Bir foydalanuvchi → Ko'p post
  Bir post          → Ko'p izoh

Many-to-Many (ko'p — ko'p):
  Ko'p post → Ko'p tag
  Ko'p tag  → Ko'p post

One-to-One (bir — bir):
  Bir foydalanuvchi → Bir profil

MongoDB da munosabatlar ikki usulda saqlanadi:

1. Reference (havola) — boshqa document ning _id si saqlanadi. PostgreSQL dagi FOREIGN KEY ga o'xshash.

2. Embed (ichiga joylashtirish) — bog'liq ma'lumot to'g'ridan-to'g'ri document ichida saqlanadi.

Qachon reference, qachon embed — bu muhim qaror:

Reference ishlating:
  ✓ Bog'liq ma'lumot alohida so'raladi
  ✓ Ko'p joyda takrorlanadi (user ma'lumoti)
  ✓ Ko'p yozuvlar bo'ladi (postlar soni cheksiz)

Embed ishlating:
  ✓ Har doim birga o'qiladi (manzil → user bilan)
  ✓ Kam o'zgaradi
  ✓ Cheklangan miqdor (foydalanuvchi manzillari: 1-5 ta)

Loyiha tuzilishi

src/
├── app.module.ts
├── users/
│   ├── users.module.ts
│   ├── users.service.ts
│   ├── users.controller.ts
│   ├── schemas/
│   │   └── user.schema.ts
│   └── dto/
│       └── create-user.dto.ts
│
└── posts/
    ├── posts.module.ts
    ├── posts.service.ts
    ├── posts.controller.ts
    ├── schemas/
    │   └── post.schema.ts
    └── dto/
        └── create-post.dto.ts

Har bir resurs — o'z papkasida. Oldingi darsda o'rnatgan tuzilish.


One-to-Many — bir foydalanuvchi, ko'p post

Post sxemasi — ref bilan

// src/posts/schemas/post.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';

export type PostDocument = HydratedDocument<Post>;

@Schema({ timestamps: true })
export class Post {
  @Prop({ required: true })
  title: string;

  @Prop({ required: true })
  content: string;

  @Prop({ default: false })
  published: boolean;

  // User ga havola — ref shu yerda
  @Prop({ type: Types.ObjectId, ref: 'User', required: true })
  author: Types.ObjectId;
}

export const PostSchema = SchemaFactory.createForClass(Post);

ref: 'User' — bu maydon users collection dagi hujjatga havola ekani. Types.ObjectId — MongoDB ning ID tipi.

Posts Module — UsersModule import qilinmaydi

// src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { Post, PostSchema } from './schemas/post.schema';

@Module({
  imports: [
    MongooseModule.forFeature([
      { name: Post.name, schema: PostSchema },
    ]),
  ],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

DTO

// src/posts/dto/create-post.dto.ts
import { IsString, IsBoolean, IsOptional, IsMongoId } from 'class-validator';

export class CreatePostDto {
  @IsString()
  title: string;

  @IsString()
  content: string;

  @IsOptional()
  @IsBoolean()
  published?: boolean;

  @IsMongoId() // ← MongoDB ID formatini tekshiradi
  author: string;
}

@IsMongoId() — foydalanuvchi author maydoniga noto'g'ri format yubora olmasligi uchun.

Posts Service — populate bilan

// src/posts/posts.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Post, PostDocument } from './schemas/post.schema';
import { CreatePostDto } from './dto/create-post.dto';

@Injectable()
export class PostsService {
  constructor(
    @InjectModel(Post.name) private postModel: Model<PostDocument>,
  ) {}

  async create(dto: CreatePostDto): Promise<Post> {
    const post = new this.postModel(dto);
    return post.save();
  }

  // populate — author ID o'rniga user ma'lumotlarini keltiradi
  async findAll(): Promise<Post[]> {
    return this.postModel
      .find()
      .populate('author', 'name email') // ← faqat name va email
      .sort({ createdAt: -1 })
      .exec();
  }

  async findOne(id: string): Promise<Post> {
    const post = await this.postModel
      .findById(id)
      .populate('author', 'name email')
      .exec();

    if (!post) throw new NotFoundException('Post topilmadi');
    return post;
  }

  // Biror foydalanuvchining barcha postlari
  async findByAuthor(authorId: string): Promise<Post[]> {
    return this.postModel
      .find({ author: authorId })
      .populate('author', 'name email')
      .sort({ createdAt: -1 })
      .exec();
  }
}

populate qanday ishlaydi?

populate olmagan holda:
{
  _id: "64c...",
  title: "Mening birinchi postim",
  author: "64a..."          ← faqat ID
}

populate('author', 'name email') bilan:
{
  _id: "64c...",
  title: "Mening birinchi postim",
  author: {                 ← to'liq ma'lumot
    _id: "64a...",
    name: "Jasur",
    email: "jasur@gmail.com"
  }
}

populate('author', 'name email') — ikkinchi argument maydonlar tanlovi. Faqat name va email keladi, password va boshqalar kelmaydi. Bo'sh qoldirsangiz — hamma maydon keladi.


Nested populate — zanjirli bog'liqlik

Post → Author (User), Post → Comments → Comment Author ham to'ldirilishi kerak bo'lsa:

Izoh sxemasi

// src/comments/schemas/comment.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';

export type CommentDocument = HydratedDocument<Comment>;

@Schema({ timestamps: true })
export class Comment {
  @Prop({ required: true })
  text: string;

  @Prop({ type: Types.ObjectId, ref: 'User', required: true })
  author: Types.ObjectId;

  @Prop({ type: Types.ObjectId, ref: 'Post', required: true })
  post: Types.ObjectId;
}

export const CommentSchema = SchemaFactory.createForClass(Comment);

Post sxemasiga izohlar qo'shish

// post.schema.ts — comments maydon qo'shamiz
@Prop({ type: [{ type: Types.ObjectId, ref: 'Comment' }], default: [] })
comments: Types.ObjectId[];

Zanjirli populate

// posts.service.ts
async findOneWithComments(id: string): Promise<Post> {
  const post = await this.postModel
    .findById(id)
    .populate('author', 'name email')        // post muallifi
    .populate({
      path: 'comments',                       // izohlar
      populate: {
        path: 'author',                       // izoh muallifi
        select: 'name',
      },
    })
    .exec();

  if (!post) throw new NotFoundException('Post topilmadi');
  return post;
}

Natija:

{
  "_id": "64c...",
  "title": "Mening birinchi postim",
  "author": { "name": "Jasur", "email": "jasur@gmail.com" },
  "comments": [
    {
      "_id": "64d...",
      "text": "Ajoyib post!",
      "author": { "name": "Malika" }
    }
  ]
}

Virtual — teskari bog'liqlik

Tasavvur qiling: User da posts maydoni yo'q, lekin "bu foydalanuvchining barcha postlarini" olmoqchisiz. Har safar postModel.find({ author: userId }) yozish o'rniga — virtual ishlating.

Virtual — bazada saqlanmaydi, lekin populate qilganda hisoblanadi.

// src/users/schemas/user.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type UserDocument = HydratedDocument<User>;

@Schema({ timestamps: true })
export class User {
  @Prop({ required: true })
  name: string;

  @Prop({ required: true, unique: true })
  email: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

// Virtual — sxemadan keyin qo'shiladi
UserSchema.virtual('posts', {
  ref: 'Post',           // qaysi model
  localField: '_id',     // User da qaysi maydon
  foreignField: 'author', // Post da qaysi maydon bilan bog'liq
});

Virtual ni ishlatish:

// users.service.ts
async findOneWithPosts(id: string) {
  const user = await this.userModel
    .findById(id)
    .populate('posts', 'title published createdAt') // ← virtual populate
    .exec();

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

Muhim: Virtual toJSON/toObject ga qo'shilishi kerak, aks holda javobda ko'rinmaydi:

@Schema({ timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } })
export class User { ... }

Many-to-Many — ko'p-ko'p munosabat

Post va Tag: bitta postda ko'p tag, bitta tagda ko'p post.

// src/tags/schemas/tag.schema.ts
@Schema({ timestamps: true })
export class Tag {
  @Prop({ required: true, unique: true })
  name: string;
}
// post.schema.ts — tags maydoni qo'shamiz
@Prop({ type: [{ type: Types.ObjectId, ref: 'Tag' }], default: [] })
tags: Types.ObjectId[];
// posts.service.ts
async create(dto: CreatePostDto): Promise<Post> {
  const post = new this.postModel(dto);
  return post.save();
}

// Tag ID lar massivi bilan post yaratish
// dto.tags = ['64e...', '64f...']

async findAllWithTags(): Promise<Post[]> {
  return this.postModel
    .find()
    .populate('tags', 'name')
    .populate('author', 'name')
    .exec();
}

Natija:

{
  "title": "NestJS darsi",
  "author": { "name": "Jasur" },
  "tags": [
    { "name": "nestjs" },
    { "name": "backend" }
  ]
}

Embed — ichiga joylashtirish

Ba'zan alohida collection shart emas. Masalan, foydalanuvchi manzillari — har doim user bilan birga o'qiladi, alohida so'rov kerak emas:

// Alohida Schema — lekin model emas
const AddressSchema = new Schema({
  city:    { type: String, required: true },
  street:  { type: String, required: true },
  zipCode: { type: String },
});

@Schema({ timestamps: true })
export class User {
  @Prop({ required: true })
  name: string;

  // Embed — to'g'ridan-to'g'ri ichida
  @Prop({ type: AddressSchema })
  address: {
    city: string;
    street: string;
    zipCode?: string;
  };

  // Massiv embed
  @Prop({
    type: [AddressSchema],
    default: [],
  })
  savedAddresses: {
    city: string;
    street: string;
    zipCode?: string;
  }[];
}

Manzil alohida ID ga ega emas, alohida so'rov kerak emas — populate ham kerak emas:

// Natija
{
  "name": "Jasur",
  "address": {
    "city": "Toshkent",
    "street": "Navoiy ko'chasi, 5"
  }
}

Tez-tez uchraydigan xatolar

populate ishlashi uchun ref to'g'ri yozilishi kerak

// ❌ ref va model nomi mos kelmasa populate ishlamaydi
@Prop({ type: Types.ObjectId, ref: 'Users' }) // ← "Users" emas
author: Types.ObjectId;

// ✅ Sxema klassining nomi bilan bir xil bo'lishi kerak
@Prop({ type: Types.ObjectId, ref: 'User' }) // ← "User" — sinf nomi
author: Types.ObjectId;

Virtual ko'rinmasa

// ❌ toJSON virtuals yo'q — javobda virtual bo'lmaydi
@Schema({ timestamps: true })
export class User { ... }

// ✅ toJSON va toObject ga virtuals: true qo'shiladi
@Schema({
  timestamps: true,
  toJSON: { virtuals: true },
  toObject: { virtuals: true },
})
export class User { ... }

populate da keraksiz maydonlar kelishi

// ❌ Hamma maydon keladi — parol ham
.populate('author')

// ✅ Faqat keraklilari
.populate('author', 'name email -password')
// '-password' — minusli maydon olib tashlanadi

Xulosa

Mongoose da munosabatlar uch usulda boshqariladi:

ref + populate   →  alohida collection, ID orqali bog'liq
                    (foydalanuvchi → postlar)

virtual          →  teskari bog'liqlik, bazada saqlanmaydi
                    (user → uning postlari, ID user da yo'q)

embed            →  document ichida to'g'ridan-to'g'ri
                    (user → manzil, birga o'qiladi)

Qoidasi oddiy: bog'liq ma'lumot alohida so'raladigan va ko'p bo'ladigan bo'lsa — ref. Har doim birga keladigan, kam bo'ladigan bo'lsa — embed.


Keyingi dars

24-dars: Soft Delete va Pagination, Filtering