Database Relations (Mongoose)
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