Virtuals — virtual field, virtual populate

~20 daq · MongoDB · Mongoose — Ilg'or

Muammodan boshlaylik

User da ism va familiya alohida saqlanadi. Ko'p joyda to'liq ism kerak:

`${user.ism} ${user.familiya}`    // 15 ta faylda takrorlangan

Bazaga toliqIsm maydonini qo'shsangiz — ism o'zgarganda uni ham yangilashni unutasiz va ular farqlanib qoladi.

Ikkinchi muammo: user ning postlari. Userda postlar: [ObjectId] massivi saqlansa — har bir yangi post uchun userni ham yangilash kerak, massiv esa cheksiz o'sadi.


Virtual nima?

Virtual — bazada saqlanmaydigan, lekin document da maydon kabi ko'rinadigan xususiyat. Uning qiymati boshqa maydonlardan hisoblanadi.

const userSchema = new Schema({
  ism: String,
  familiya: String,
  tugilgan: Date,
})

userSchema.virtual('toliqIsm').get(function () {
  return `${this.ism} ${this.familiya}`
})

userSchema.virtual('yosh').get(function () {
  if (!this.tugilgan) return null
  const farq = Date.now() - this.tugilgan.getTime()
  return Math.floor(farq / (365.25 * 24 * 60 * 60 * 1000))
})
const user = await User.findById(id)
user.toliqIsm   // "Lola Karimova"
user.yosh       // 24

Bazada faqat ism, familiya, tugilgan. Hech narsa takrorlanmaydi va farqlanib qolmaydi.


Hayotiy o'xshatma

Pasportda tug'ilgan sana yozilgan — yosh emas. Yoshni har safar sanadan hisoblaysiz. Agar pasportga "24 yosh" deb yozilsa — keyingi yili u noto'g'ri bo'lib qoladi. Virtual — "yoshni har safar sanadan hisoblash" qoidasi.


Setter

userSchema.virtual('toliqIsm')
  .get(function () {
    return `${this.ism} ${this.familiya}`
  })
  .set(function (v) {
    const [ism, ...qolgan] = v.trim().split(' ')
    this.ism = ism
    this.familiya = qolgan.join(' ')
  })
const user = new User({ toliqIsm: 'Lola Karimova' })
user.ism        // "Lola"
user.familiya   // "Karimova"

JSON da ko'rsatish

Standart holatda virtual lar res.json(user) da ko'rinmaydi:

const userSchema = new Schema({ ... }, {
  toJSON: { virtuals: true },
  toObject: { virtuals: true },
})

lean() va virtual lar

const user = await User.findById(id).lean()
user.toliqIsm   // undefined — lean oddiy obyekt

.lean() bilan virtual lar kerak bo'lsa — mongoose-lean-virtuals plagini:

import mongooseLeanVirtuals from 'mongoose-lean-virtuals'
userSchema.plugin(mongooseLeanVirtuals)

await User.findById(id).lean({ virtuals: true })

[!WARNING] Virtual lar bo'yicha so'rov va saralash mumkin emas: User.find({ toliqIsm: 'Lola Karimova' }) — hech narsa topmaydi, chunki bazada bunday maydon yo'q. Filtr kerak bo'lsa — asl maydonlar bo'yicha qidiring yoki maydonni haqiqatan saqlang.


Virtual populate

Teskari bog'lanish: post userni biladi (muallif), user esa postlari haqida hech narsa saqlamaydi. Lekin userning postlarini populate qilish mumkin:

const postSchema = new Schema({
  sarlavha: String,
  muallif: { type: Schema.Types.ObjectId, ref: 'User', index: true },
  nashr: Boolean,
})

userSchema.virtual('postlar', {
  ref: 'Post',               // qaysi model
  localField: '_id',         // userdagi maydon
  foreignField: 'muallif',   // Post dagi maydon
})
const user = await User.findById(id).populate({
  path: 'postlar',
  select: 'sarlavha createdAt',
  match: { nashr: true },
  options: { sort: { createdAt: -1 }, limit: 10 },
})

user.postlar   // [{ sarlavha: "...", createdAt: ... }, ...]
  • Userda massiv saqlanmaydi — cheksiz o'sish va sinxronlash muammosi yo'q
  • Post yaratilganda faqat Post yoziladi
  • foreignField ga index qo'ying

Sanash

userSchema.virtual('postlarSoni', {
  ref: 'Post',
  localField: '_id',
  foreignField: 'muallif',
  count: true,
})

const user = await User.findById(id).populate('postlarSoni')
user.postlarSoni   // 42

Bitta document

userSchema.virtual('profil', {
  ref: 'Profil',
  localField: '_id',
  foreignField: 'user',
  justOne: true,             // massiv emas, bitta document yoki null
})

Qachon virtual, qachon saqlash

Virtual Saqlangan maydon
Boshqa maydonlardan arzon hisoblanadi Hisoblash qimmat (masalan, ko'p documentdan yig'indi)
Faqat ko'rsatish uchun Filtrlash, saralash, index kerak
Doim yangi bo'lishi kerak (yosh) Bir lahzadagi qiymat (buyurtma paytidagi narx)
// ✅ virtual — arzon va doim to'g'ri
toliqIsm, yosh, chegirmaFoizi

// ✅ saqlangan — saralash va filtr kerak
reyting (o'rtacha baho), postlarSoni (ro'yxatda saralash uchun)

[!TIP] Saralash kerak bo'lgan hisoblangan maydonni saqlash va uni middleware (46-dars) yoki $inc bilan yangilab borish — keng tarqalgan yondashuv: masalan, post.izohlarSoni har yangi izohda $inc: 1.


Tez-tez uchraydigan xatolar

Arrow function

// ❌ — this undefined
userSchema.virtual('toliqIsm').get(() => `${this.ism} ${this.familiya}`)

toJSON: { virtuals: true } ni unutish

API javobida virtual yo'q — "lekin konsolda bor edi!".

select da virtual uchun kerakli maydonni chiqarib tashlash

await User.findById(id).select('email')
// toliqIsm → "undefined undefined"

Virtual hisoblanadigan maydonlar ham tanlangan bo'lishi kerak.


Xulosa

  • Virtual — bazada saqlanmaydigan, boshqa maydonlardan hisoblanadigan xususiyat
  • schema.virtual('nom').get(function () {...}).set(...) — oddiy function
  • JSON da ko'rinishi uchun — toJSON: { virtuals: true }; lean() da — plagin
  • Virtual bo'yicha filtr va saralash mumkin emas
  • Virtual populate — teskari bog'lanish massiv saqlamasdan: ref, localField, foreignField
  • count: true — sanash, justOne: true — bitta document

Keyingi dars: 46-dars — Middleware (Hooks) — pre, post, save, find, remove