Soft Delete va Pagination, Filtering
Muammodan boshlaylik
Foydalanuvchi o'z hisobini o'chirdi. Siz deleteOne qildingiz — ma'lumot yo'qoldi. Bir haftadan keyin u texnik yordam bilan murojaat qildi: "Hisobimni tiklashim mumkinmi?" Javob: "Yo'q, ma'lumotlar yo'q."
Boshqa muammo: frontend /users endpointga so'rov yubordi. Bazada 50 000 ta foydalanuvchi bor. Hammasi bitta javobda keldi — brauzer muzlab qoldi.
Bu ikki muammoni Soft Delete va Pagination hal qiladi.
Soft Delete — o'chirmasdan yashirish
Soft Delete — ma'lumotni aslida o'chirmaslik. O'rniga deletedAt maydoniga sana yoziladi. So'rovlarda deletedAt: null bo'lganlar ko'rsatiladi — o'chirilganlar ko'rinmaydi, lekin bazada qoladi.
Hayotiy o'xshatma: arxiv xonasi. Hujjatni yo'q qilish o'rniga "arxivlangan" tamg'asi bosiladi va alohida tortmachaga qo'yiladi. Kerak bo'lsa topiladi, odatda ko'rinmaydi.
Mongoose bilan Soft Delete
// src/users/schemas/user.schema.ts
@Schema({ timestamps: true })
export class User {
@Prop({ required: true })
name: string;
@Prop({ required: true, unique: true })
email: string;
@Prop({ default: null })
deletedAt: Date | null; // ← null = faol, Date = o'chirilgan
}
// src/users/users.service.ts
@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
) {}
// Faqat o'chirilmaganlarni ko'rsatish
async findAll(): Promise<User[]> {
return this.userModel
.find({ deletedAt: null }) // ← asosiy filter
.exec();
}
// Soft delete — aslida o'chirmaslik
async remove(id: string): Promise<{ message: string }> {
const user = await this.userModel.findOneAndUpdate(
{ _id: id, deletedAt: null }, // faqat faol foydalanuvchi
{ deletedAt: new Date() }, // ← sana yoziladi
{ new: true },
).exec();
if (!user) throw new NotFoundException('Foydalanuvchi topilmadi');
return { message: 'O\'chirildi' };
}
// Tiklash
async restore(id: string): Promise<User> {
const user = await this.userModel.findOneAndUpdate(
{ _id: id, deletedAt: { $ne: null } }, // faqat o'chirilganlar
{ deletedAt: null }, // ← null ga qaytariladi
{ new: true },
).exec();
if (!user) throw new NotFoundException('Topilmadi yoki allaqachon faol');
return user;
}
// Admin uchun: o'chirilganlarni ham ko'rish
async findAllWithDeleted(): Promise<User[]> {
return this.userModel.find().exec(); // filter yo'q
}
}
Mongoose Middleware bilan avtomatlashtirish
Har find da { deletedAt: null } yozishdan charchadingizmi? Mongoose middleware yordamida avtomatik qo'shiladi:
// user.schema.ts — sxemadan keyin
UserSchema.pre(/^find/, function (next) {
// "this" — joriy query
this.where({ deletedAt: null }); // ← har find da avtomatik
next();
});
Endi findAll, findById, findOne — barchasi avtomatik deletedAt: null bilan ishlaydi. Alohida yozish shart emas.
Pagination — sahifalab berish
50 000 ta yozuvni bitta javobda yuborish — server va brauzer uchun ham og'ir. Pagination ma'lumotni bo'laklarga bo'ladi.
Offset-based Pagination
Eng keng tarqalgan usul. page va limit query params orqali:
GET /users?page=2&limit=10
→ 11-20 yozuvlar
// src/users/dto/pagination.dto.ts
import { IsOptional, IsInt, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
export class PaginationDto {
@IsOptional()
@Type(() => Number) // query string → number
@IsInt()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number = 10;
}
// users.service.ts
async findAll(dto: PaginationDto) {
const { page = 1, limit = 10 } = dto;
const skip = (page - 1) * limit; // ← nechtasini o'tkazish
const [data, total] = await Promise.all([
this.userModel
.find({ deletedAt: null })
.sort({ createdAt: -1 })
.skip(skip) // ← o'tkazib yuborish
.limit(limit) // ← nechta olish
.exec(),
this.userModel.countDocuments({ deletedAt: null }),
]);
return {
data,
meta: {
total, // jami yozuvlar soni
page, // joriy sahifa
limit, // sahifada nechta
totalPages: Math.ceil(total / limit),
hasNext: page < Math.ceil(total / limit),
hasPrev: page > 1,
},
};
}
// users.controller.ts
@Get()
findAll(@Query() dto: PaginationDto) {
return this.usersService.findAll(dto);
}
Javob:
{
"data": [ { "_id": "...", "name": "Jasur" }, ... ],
"meta": {
"total": 50000,
"page": 2,
"limit": 10,
"totalPages": 5000,
"hasNext": true,
"hasPrev": true
}
}
Filtering — so'rovni filtrlash
Foydalanuvchilar ro'yxatini isim, email yoki holat bo'yicha filtrlash:
GET /users?name=jasur&isActive=true&sort=createdAt&order=desc
// src/users/dto/filter-user.dto.ts
import { IsOptional, IsString, IsBoolean, IsIn } from 'class-validator';
import { Transform, Type } from 'class-transformer';
export class FilterUserDto extends PaginationDto {
@IsOptional()
@IsString()
name?: string; // qisman qidirish uchun
@IsOptional()
@IsString()
email?: string;
@IsOptional()
@Transform(({ value }) => value === 'true') // "true" → true
@IsBoolean()
isActive?: boolean;
@IsOptional()
@IsIn(['createdAt', 'name', 'email'])
sort?: string = 'createdAt';
@IsOptional()
@IsIn(['asc', 'desc'])
order?: 'asc' | 'desc' = 'desc';
}
// users.service.ts
async findAll(dto: FilterUserDto) {
const { page = 1, limit = 10, name, email, isActive, sort = 'createdAt', order = 'desc' } = dto;
const skip = (page - 1) * limit;
// Filter qurish
const filter: any = { deletedAt: null };
if (name) {
filter.name = { $regex: name, $options: 'i' }; // ← katta-kichik harfsiz qidirish
}
if (email) {
filter.email = { $regex: email, $options: 'i' };
}
if (isActive !== undefined) {
filter.isActive = isActive;
}
const sortObj = { [sort]: order === 'desc' ? -1 : 1 };
const [data, total] = await Promise.all([
this.userModel
.find(filter)
.sort(sortObj)
.skip(skip)
.limit(limit)
.exec(),
this.userModel.countDocuments(filter),
]);
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
hasNext: page < Math.ceil(total / limit),
hasPrev: page > 1,
},
};
}
Tez-tez uchraydigan xatolar
skip katta bo'lsa sekinlashadi
// ❌ skip(49990) — MongoDB 49990 ta hujjatni sanab o'tadi
.skip(49990).limit(10)
// ✅ Katta ma'lumotlar uchun cursor-based pagination
// Oxirgi elementning _id sini saqlang, keyingisini shundan qidiring:
.find({ _id: { $gt: lastId }, deletedAt: null })
.limit(10)
Query string dan boolean olish
// ❌ query string da "true" — bu string, boolean emas
// isActive === true — hech qachon true bo'lmaydi
const { isActive } = req.query; // "true" string
// ✅ Transform dekoratori yoki qo'lda aylantirish
@Transform(({ value }) => value === 'true')
@IsBoolean()
isActive?: boolean;
countDocuments va find ni parallel ishlatmaslik
// ❌ Ketma-ket — ikkinchisi birinchisi tugaguncha kutadi
const data = await this.userModel.find(filter)...
const total = await this.userModel.countDocuments(filter);
// ✅ Parallel — ikkalasi birga ishlaydi
const [data, total] = await Promise.all([
this.userModel.find(filter)...,
this.userModel.countDocuments(filter),
]);
Xulosa
Soft Delete — deletedAt maydoni bilan ma'lumotni yashirish. O'chirilgan narsa bazada qoladi, tiklash mumkin.
Pagination — skip va limit bilan ma'lumotni bo'laklash. meta obyektida total, page, totalPages qaytariladi.
Filtering — dinamik filter obyekti qurib, regex bilan qisman qidirish.
Eslab qoling:
deletedAt: null— faol yozuvlar filtriskip = (page - 1) * limit— o'tkazib yuborish formulasiPromise.all([find, countDocuments])— parallel so'rovlar$regex+$options: 'i'— katta-kichik harfsiz qidirish@Transform— query string ni to'g'ri tipga aylantirish
Keyingi dars
25-dars: PassportJS — asoslar va strategiyalar