Mongoose ODM + MongoDB — CRUD

~30 daq · NestJS · Ma'lumotlar bazasi

Muammodan boshlaylik

TypeORM va Prisma darslari murakkab tuyulishi mumkin: migration fayllar, schema.prisma, adapter lar, entity dekoratorlar... MongoDB va Mongoose bilan bular yo'q.

Mongoose da siz faqat ikkita narsani yozasiz: Schema (ma'lumot qanday ko'rinishda bo'ladi) va Service (u bilan nima qilasiz). Hammasi shu.


Mongoose nima? MongoDB bilan farqi?

MongoDB — ma'lumotlar bazasi. U ma'lumotlarni JSON-ga o'xshash hujjatlar sifatida saqlaydi. Jadvallar yo'q — collection lar bor. Qatorlar yo'q — document lar bor.

PostgreSQL (jadval):           MongoDB (collection):
┌────┬────────┬──────────────┐  { _id: "64a...", name: "Jasur",
│ id │  name  │    email     │    email: "jasur@gmail.com",
├────┼────────┼──────────────┤    age: 24 }
│  1 │ Jasur  │ j@gmail.com  │
│  2 │ Malika │ m@gmail.com  │  { _id: "64b...", name: "Malika",
└────┴────────┴──────────────┘    email: "malika@gmail.com" }

Mongoose — MongoDB uchun ODM (Object Document Mapper). U MongoDB ning past darajali drayveridan ustida ishlaydi va qulay interfeys beradi: Schema, validatsiya, metodlar.

Hayotiy o'xshatma: MongoDB — xom ma'lumotlar ombori, Mongoose — omborchining yordamchisi. Yordamchi narsalarni tartibda saqlaydi, noto'g'ri narsa qo'ymoqchi bo'lsangiz to'xtatadi, kerak bo'lganda tez topib beradi.


Loyiha tuzilishi — toza arxitektura

Har bir resurs uchun alohida papka. Hamma narsa o'z joyida:

src/
├── app.module.ts
├── main.ts
│
└── users/                        ← bitta resurs = bitta papka
    ├── users.module.ts           ← modulni birlashtiradi
    ├── users.controller.ts       ← HTTP so'rovlar
    ├── users.service.ts          ← biznes mantiq
    ├── schemas/
    │   └── user.schema.ts        ← MongoDB schema
    └── dto/
        ├── create-user.dto.ts    ← yaratish uchun ma'lumot shakli
        └── update-user.dto.ts    ← yangilash uchun ma'lumot shakli

Bu tuzilish — NestJS da standart. Schema va DTO alohida papkalarda chunki ular har xil vazifani bajaradi: Schema — baza bilan gaplashadi, DTO — foydalanuvchi bilan.


O'rnatish

npm install @nestjs/mongoose mongoose

1. MongoDB ga ulanish — AppModule

// src/app.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost:27017/myapp'), // ← ulanish
    UsersModule,
  ],
})
export class AppModule {}

Haqiqiy loyihada URL .env dan olinadi — bu keyingi darslarda ko'ramiz. Hozircha to'g'ridan-to'g'ri yozamiz.


2. Schema — ma'lumot qanday ko'rinishda bo'ladi

Schema — MongoDB collection da har bir document qanday tuzilishda bo'lishini belgilaydi. Mongoose shu sxemaga ko'ra validatsiya qiladi.

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

// Document tipi — TypeScript uchun
export type UserDocument = HydratedDocument<User>;

@Schema({ timestamps: true }) // ← createdAt, updatedAt avtomatik qo'shiladi
export class User {
  @Prop({ required: true })
  name: string;

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

  @Prop({ required: true, select: false }) // select: false — so'rovda ko'rsatilmaydi
  password: string;

  @Prop({ default: 0 })
  age: number;

  @Prop({ default: true })
  isActive: boolean;
}

export const UserSchema = SchemaFactory.createForClass(User);

Dekoratorlar nima qiladi:

  • @Schema() — bu sinf MongoDB sxemasi ekanini bildiradi
  • @Prop() — bu maydon sxemaga qo'shiladi
  • required: true — bo'sh bo'lmasligi kerak
  • unique: true — takrorlanmasligi kerak
  • lowercase: true — saqlashdan oldin kichik harfga o'tkazadi
  • select: false — find() da parol avtomatik kelmaydi (xavfsizlik)
  • timestamps: true — createdAt va updatedAt Mongoose o'zi boshqaradi

3. DTO — foydalanuvchidan qabul qilinadigan ma'lumot shakli

DTO (Data Transfer Object) — foydalanuvchi yuboradigan ma'lumot qanday bo'lishi kerakligini belgilaydi. Schema emas, DTO validatsiya qiladi.

// src/users/dto/create-user.dto.ts
import { IsString, IsEmail, IsInt, Min, Max, IsOptional, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(6)
  password: string;

  @IsOptional()
  @IsInt()
  @Min(1)
  @Max(120)
  age?: number;
}
// src/users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

// PartialType — CreateUserDto dagi barcha maydonlarni ixtiyoriy qiladi
export class UpdateUserDto extends PartialType(CreateUserDto) {}

PartialType — juda qulay. UpdateUserDto uchun alohida hamma narsani qayta yozmasiz — CreateUserDto dan meros olib, hammasini ? (ixtiyoriy) qiladi.


4. Module — hammasini birlashtirish

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User, UserSchema } from './schemas/user.schema';

@Module({
  imports: [
    // Sxemani shu modulga ro'yxatdan o'tkazamiz
    MongooseModule.forFeature([
      { name: User.name, schema: UserSchema },
    ]),
  ],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // boshqa modullar ishlatishi uchun
})
export class UsersModule {}

5. Service — CRUD amallar

// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from './schemas/user.schema';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UsersService {
  constructor(
    @InjectModel(User.name) private userModel: Model<UserDocument>,
  ) {}

  // Yaratish
  async create(dto: CreateUserDto): Promise<User> {
    const user = new this.userModel(dto);
    return user.save();
  }

  // Barchasini olish
  async findAll(): Promise<User[]> {
    return this.userModel.find().exec();
  }

  // Bittasini ID bo'yicha olish
  async findOne(id: string): Promise<User> {
    const user = await this.userModel.findById(id).exec();
    if (!user) {
      throw new NotFoundException(`Foydalanuvchi topilmadi: ${id}`);
    }
    return user;
  }

  // Yangilash
  async update(id: string, dto: UpdateUserDto): Promise<User> {
    const user = await this.userModel
      .findByIdAndUpdate(
        id,
        dto,
        { new: true }, // ← yangilangandan keyin yangi hujjatni qaytaradi
      )
      .exec();
    if (!user) {
      throw new NotFoundException(`Foydalanuvchi topilmadi: ${id}`);
    }
    return user;
  }

  // O'chirish
  async remove(id: string): Promise<{ message: string }> {
    const user = await this.userModel.findByIdAndDelete(id).exec();
    if (!user) {
      throw new NotFoundException(`Foydalanuvchi topilmadi: ${id}`);
    }
    return { message: `${user.name} o'chirildi` };
  }
}

Muhim tushunchalar:

@InjectModel(User.name) — NestJS DI tizimiga "menga User sxemasining Mongoose Model ini ber" deydi. User.name — bu "User" degan string, sxema ro'yxatdan o'tkazilganda shu nom ishlatiladi.

{ new: true } — findByIdAndUpdate standart holda yangilanishdan oldingi hujjatni qaytaradi. new: true bilan yangilangandan keyin gi hujjat keladi. Bu ko'pincha unutiladi.

NotFoundException — NestJS ning o'rnatilgan Exception klassi. U 404 Not Found javobini avtomatik qaytaradi.


6. Controller — HTTP marshrutlash

// src/users/users.controller.ts
import {
  Controller, Get, Post, Put, Delete,
  Body, Param, HttpCode, HttpStatus,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  // POST /users
  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() dto: CreateUserDto) {
    return this.usersService.create(dto);
  }

  // GET /users
  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  // GET /users/:id
  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }

  // PUT /users/:id
  @Put(':id')
  update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
    return this.usersService.update(id, dto);
  }

  // DELETE /users/:id
  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.usersService.remove(id);
  }
}

Controller faqat so'rovni qabul qiladi va Service ga uzatadi — boshqa hech narsa qilmaydi. Bu toza arxitekturaning asosiy qoidasi.


So'rov-javob oqimi — to'liq manzara

POST /users  →  Body: { name, email, password }
      │
      ▼
UsersController.create(@Body() dto)
      │  DTO validatsiya (class-validator)
      ▼
UsersService.create(dto)
      │  new this.userModel(dto) → .save()
      ▼
MongoDB: users collection ga yozadi
      │
      ▼
Javob: { _id, name, email, age, isActive, createdAt, updatedAt }

Mongoose metodlari — qisqa ma'lumotnoma

// Yaratish
new this.userModel(dto).save()
this.userModel.create(dto)                  // ikkalasi bir xil

// O'qish
this.userModel.find()                       // barchasi
this.userModel.find({ isActive: true })     // shartli
this.userModel.findById(id)                 // ID bo'yicha
this.userModel.findOne({ email })           // bitta, shartli

// Yangilash
this.userModel.findByIdAndUpdate(id, dto, { new: true })
this.userModel.findOneAndUpdate({ email }, dto, { new: true })
this.userModel.updateMany({ isActive: false }, { isActive: true })

// O'chirish
this.userModel.findByIdAndDelete(id)
this.userModel.deleteMany({ isActive: false })

// Foydali
this.userModel.countDocuments({ isActive: true }) // soni
this.userModel.exists({ email })                   // bormi?
this.userModel.find().select('name email -password') // maydonlar
this.userModel.find().sort({ createdAt: -1 })      // tartiblash
this.userModel.find().limit(10).skip(20)            // pagination

Tez-tez uchraydigan xatolar

{ new: true } unutish

// ❌ Yangilanishdan oldingi hujjat keladi — foydalanuvchi eski ma'lumot ko'radi
const user = await this.userModel.findByIdAndUpdate(id, dto).exec();

// ✅ Yangilangandan keyin yangi hujjat
const user = await this.userModel.findByIdAndUpdate(id, dto, { new: true }).exec();

Noto'g'ri MongoDB ID formati

// ❌ "abc" kabi noto'g'ri ID da CastError xatosi chiqadi — 500 javob
await this.userModel.findById('abc');

// ✅ Oldin tekshirish
import { isValidObjectId } from 'mongoose';

async findOne(id: string) {
  if (!isValidObjectId(id)) {
    throw new NotFoundException('Noto\'g\'ri ID format');
  }
  const user = await this.userModel.findById(id).exec();
  if (!user) throw new NotFoundException('Topilmadi');
  return user;
}

Parolni ochiq qaytarish

// Schema da select: false qo'yilgan — odatda kelmaydi
@Prop({ required: true, select: false })
password: string;

// Faqat kerak bo'lganda (masalan, login tekshiruvi) — '+' bilan so'raladi
const user = await this.userModel
  .findOne({ email })
  .select('+password')
  .exec();

Xulosa

Mongoose — MongoDB bilan ishlashning eng qulay yo'li. Migration yo'q, alohida schema fayl yo'q, adapter yo'q. Faqat Schema va Service — boshqa hech narsa.

Toza arxitektura qoidasi:

Controller  →  faqat HTTP (so'rov qabul, javob yuborish)
Service     →  faqat biznes mantiq (CRUD, hisoblash)
Schema      →  faqat baza tuzilishi
DTO         →  faqat kiritma validatsiya

Har birining roli aniq, har biri o'z ishini qiladi — boshqasiga aralashmaydi.


Keyingi dars

23-dars: Database Relations (Mongoose) — ref, populate, virtual