Skip to main content

MongoDB

یک دیتابیس nosql هست که درصورتی که relation داخلش زیاد نباشه سرعت بسیار خوبی داره و درصورت زیاد شدن relation ها سرعتش به طور محسوسی کم میشه

توی این داکیومنت ما پیاده‌سازی MongoDB درون Node.js رو میگیم درحالی که در بقیه‌ی زبان ها هم قابل استفاده و پیاده‌سازی هست

از طرفی دیتابیس MongoDB کدها و کوئری‌هاش خیلی شبیه زبان Javascript هست

Start

ما از ODM در Node.js برای راحتی کار استفاده میکنیم که بدونیم مدل سازی کنیم و مدل بسازیم

Install

ما از Mongoose به عنوان ODM استفاده میکنی

npm install mongoose

Definitions

ما اینجا برخی از تعاریف دیتابیس‌های nosql رو آوردیم

  • Collection : همون معنی table در دیتابیس sql-ای رو میده

  • Document : معنی یک سطر داخل دیتابیس sql-ای رو میده که درواقع یک عضو از یک Collection هست

  • ORM : ابزاری برای اتصال به دیتابیس‌های sql ای هست که نیاز نباشه کد‌های مختلف برای دیتابیس‌های مختلف زد و یک کد روی انواع دیتابیس‌های sql ای کار میکنه

  • ODM : معادل ORM برای دیتابیس‌های nosql هست که D به خاطر Document در دیتابیس nosql هست

Connect to Database

درصورتی که دیتابیسی که اسمشو میزنیم وجود نداشته باشه اونو میسازه

ما برای کانفیگ کانکشن به دیتابیس در دایرکتوری configs یک فایل به اسم mongodb.config.js میسازیم

configs/mongodb.config.js
const { default: mongoose } = require("mongoose");

const DB_URL = "mongodb://localhost:27017";
const DB_NAME = "mongodb-learning";
const URL = `${DB_URL}/${DB_NAME}`;

const ConnectDB = async () => {
try {
await mongoose.connect(URL);
console.log("Connected to DB Successfully");
} catch (err) {
console.log(err.message);
}
};

module.exports = ConnectDB;

سپس از این کانفیگ درون server.js یا فایل اصلی پروژه برای اتصال به دیتابیس استفاده میکنیم

server.js
const ConnectDB = require("./configs/mongodb.config");

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
ConnectDB();

// Routes, Others ...

Collection

Create Collection

model همون معادل table در دیتابیس‌های sql ای هست که توسط اسکیما ساخته میشود

در این قسمت ما یک collection را schema بهش اختصاص میدیم و میسازیمش

ما برای تمام model ها یک دایرکتوری با اسم models میسازیم و تمام مدل‌ها را درون آن قرار میدهیم. برای مثال برای مدل کاربران یک فایل با اسم users.mode.js میسازیم و داخل آن schema مرتبط به یوزرها رو تعریف میکنیم

models/user.model.js
const { Schema, model } = require("mongoose");

const UserSchema = new Schema(
{
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
},
{
timestamps: true,
},
);
const UserModel = model("user", UserSchema);
module.exports = UserModel;

برخی فلگ‌ها و محدودیت‌ها توی فیلدها میتونیم اعمال کنیم مثل required و .... برخیشون رو اینجا اوردیم

{
"type" : String, // Boolean, Number, Schema, [String], [Number], ...
"required": true,
"unique": true,
"trim": true,
"minLength": 10,
"maxLength": 12,
"default": "default value"
}

Insert One Document

این متد معادل Insert درون دیتابیس‌های sql ای هست. یعنی یک داکیومنت به collection اضافه میکنیم

میتونیم با ۲ تا تابع create و یا insertOne این کارو انجام بدیم

create :

await ModelName.create({
// fields ...
});

insertOne :

await ModelName.insertOne({
// fields ...
});

برای مثال برای درج یک یوزر درون user collection ما به صورت زیر عمل میکنیم :

example
const result = await UserModel.create({
username: "amirhossein",
password: "iran12Sa@!3",
});

همچنین با insertOne هم میشه انجام داد

example
const result = await UserModel.insertOne({
username: "amirhossein",
password: "iran12Sa@!3",
});

مقدار result برابر نتیجه هست که شامل فیلد ساخته شده هست

result
{
"username": "amir",
"password": "ali",
"_id": "69ad61b013e6bde575e146f0",
"createdAt": "2026-03-08T11:46:56.960Z",
"updatedAt": "2026-03-08T11:46:56.960Z",
"__v": 0
}

همانطور که مشاهده میشود _id رو هم برگردونده و درواقع داکیومنت درج شده در collection رو به طور کامل برگردوند

Insert Many Document

با تابع insertMany چند داکیومنت اضافه میکنیم به کالکشن

await ModelName.insertMany([
{
// item1 ...
},
{
// item2 ...
},
// ...
]);

برای مثال برای درست کردن چند داکیومنت برای کالکشن users میتونیم از کد زیر استفاده کنیم

example
const result = await UserModel.insertMany([
{
username: "amirhossein",
password: "password1",
},
{
username: "aliakbar",
password: "password2",
},
]);

مقدار result برابر آرایه‌ای از داکیومنت‌های ساخته شده است

result
[
{
"username": "amabazari",
"password": "amierqw132",
"_id": "69ad708e127cb6086ca34d00",
"__v": 0,
"createdAt": "2026-03-08T12:50:22.490Z",
"updatedAt": "2026-03-08T12:50:22.490Z"
},
{
"username": "amabazari2",
"password": "amierqw132",
"_id": "69ad708e127cb6086ca34d01",
"__v": 0,
"createdAt": "2026-03-08T12:50:22.491Z",
"updatedAt": "2026-03-08T12:50:22.491Z"
}
]

Find Document

همون وظیفه‌ی SELECT توی دیتابیس sql داره

await ModelName.find();

اگیر بخوایم یک آیتم برگردونه میتونی از findOne استفاده کنی که فقط یک آبجکت برمیگردونه

await ModelName.findOne();

توی این نمونه تمام داکیومنت‌های مدل ModelName رو برمیگردونه

میتونیم شرط بزاریم که چه داکیومنت‌هایی و چه بخش‌هایی از داکیومنت رو برگردونه

await ModelName.find({ conditions }, { returns });

میتونیم شرط بزاریم برای آیتم‌هایی که برمیگردونه برای مثال آیتم‌هایی رو برگردونه که password اشون برابر علی باشه

await UserModel.find({ password: "ali" });

بعضی کوئری‌ها هم میتونیم بزنیم

await UserModel.find({ age: { $gte: 18 } }); // greater than equal : age >= 18
await UserModel.find({ age: { $gt: 18 } }); // greater than : age > 18
await UserModel.find({ age: { $lte: 18 } }); // less than equal : age <= 18
await UserModel.find({ age: { $lt: 18 } }); // less than : age < 18
await UserModel.find({ age: { $not: 32 } }); // less than : age != 32
await UserModel.find({ age: { $regex: / regex / } }); // less than : age == regex

میتونیم شرط بزاریم

میتونیم بگیم چه مقدایری از داکیومنت رو میخوایم

برای مثال اگر من توی نتیجه فقط username رو بخوام باید بزنم :

example
const result = await UserModel.find({}, { username: true });

و مقدار result برابر زیر هست:

result
[
{
"_id": "69ad61b013e6bde575e146f0",
"username": "amabazari"
},
{
"_id": "69ad6e7913b9d8b9c2953343",
"username": "amabazari2"
}
]

نکته: همواره مقدار _id برگردونده میشود

Lean Document

به طور پیش‌فرض مقداری که برگردونده میشه شامل عبارت‌های زیادی هست اگر ما هنگام find کردن یک داکیومنت به انتها‌ی اون متد lean رو کال کنیم پرفورمنس MongoDB بالا میره و بهتر میتونه عمل بکنه و چیزایی که نیاز نداریم رو حذف میکنه

await ModelName.findOne(condition).lean();

توی این حالت خیلی پرفورمنس بالاتر میره

برای مثال برای پیدا کردن یک یوزر با موبایلش به صورت زیر عمل میکنیم

example
const user = await UserModel.findOne({ _id: id, mobile }).lean();

در اینجا مقدار خروجی user شامل آیتم‌هایی بهینه تر هست. دقت شود که یک ساختار داخلی هست و ما خیلی متوجهش نمیشیم

Delete Document

برای حذف داکیومنت استفاده میشه که هم میتونیم از deleteOne و هم از deleteMany استفاده کنیم

  • deleteOne :
await ModelName.deleteOne({ condition });
  • deleteMany :
await ModelName.deleteMany({ condition });

برای مثال برای حذف یک یوزرهایی که اسمشون برابر امیرحسین و سنشون از ۱۸ سال کمتر هست میتونیم از کد زیر استفاده کنیم

const result = await UserModel.deleteMany({
name: "amirhossein",
age: { $lt: 18 },
});

خروجی result ۲ مقدار برمیگردونه که دومی برابر تعداد داکیومنت‌هایی هست که حذف شدن

{
"acknowledged": true,
"deletedCount": 12
}

توی نمونه‌ی بالا یعنی ۱۲ داکیومنت از کالکشن user حذف شده

Update Document

برای آپدیت داکیومنت استفاده میشه که هم میتونیم از updateOne و هم از updateMany استفاده کنیم

  • updateOne :
await UserModel.updateOne(condition, updates);
  • updateMany :
await UserModel.updateMany(condition, updates);

توی condition درواقع باید مشخص کنیم چه داکیومنتی رو میخوایم آپدیت کنیم و توی updates باید مقادیر جدیدی که میخوایم رو طبق برخی اتتریبیوت ها بهش بدیم. برای مثال اگر میخوایم یک فیلدش رو عوض کنیم باید از $set : {} استفاده کنیم

برای مثال میخواهیم تمام کاربران زیر ۱۸ سال را پسوردشان رو عوض کنیم

const result = await UserModel.updateOne(
{
age: { $lt: 18 },
},
{
$set: { password: "new password" },
},
);

مقدار result هم فرمتی مثل زیر داره :

{
"acknowledged": true,
"modifiedCount": 1,
"upsertedId": null,
"upsertedCount": 0,
"matchedCount": 1
}

در اینجا ما مقدار password را با استفاده از set عوض میکنیم. متد های دیگر مانند unset, push, min, max و ... وجود دارد که هرکدام کاربرد خودشون رو دارن.

Find One And

ما یک متد به فرمت findOneAnd داریم که مقدار داکیومنت رو قبل از اعمال تغییر برمیگردونه

برای مثال اگر ما findOneAndDelete بزنیم داکیومنت رو حذف میکنه و توی result مقدار داکیومنت قبل از حذف رو میده

یا ما findOneAndUpdate اگر بزنیم داکیومنت رو اپدیت میکنه و مقدار داکیومنت رو قبل از آپدیت برمیگردونه

برای مثال اگر بخوایم پسورد یک کاربر رو که mypass بوده رو به newpass تغییر بدیم و از findOneAndUpdate استفاده کنیم مقدار پسورد درون مقدار result برابر mypass یا همون پسورد قبل از آپدیت هست

مقدار حال حاضر :

{
"_id": "69ad708e127cb6086ca34d00",
"username": "amabazari",
"password": "mypass",
"__v": 0,
"createdAt": "2026-03-08T12:50:22.490Z",
"updatedAt": "2026-03-08T14:22:21.468Z"
}

کد آپدیت :

const result = await UserModel.findOneAndUpdate(
{
_id: "69ad708e127cb6086ca34d00",
},
{
$set: { password: "newpass" },
},
);

مقدار result:

{
"_id": "69ad708e127cb6086ca34d00",
"username": "amabazari",
"password": "mypass",
"__v": 0,
"createdAt": "2026-03-08T12:50:22.490Z",
"updatedAt": "2026-03-08T14:22:21.468Z"
}

همانطور که مشاهده میشود متد findOneAndUpdate ابتدا مقدار را پیدا میکند سپس درون result قرار میدهد و درنهایت آپدیت میکند

Options

اینجا برخی آپشن‌های دیگر MongoDB رو دربارش حرف میزنیم که توی پکیج mongoose هستن

Types

انواع تایپ‌هایی رو که میتونه یک فیلد از داکیومنت داشته باشه رو میگیم

const SchemaName = new Schema({
name1: { type: String },
name2: { type: Number },
name3: { type: Boolean },
name4: { type: [String] }, // array of strings
name5: { type: Types.ObjectId }, // _id type
name6: { type: [Types.ObjectId] },
});

Virtual & Populate

گاهی وقت‌ها نیاز هست ما بین ۲ داکیومنت یک ارتباط برقرار کنیم. برای مثال در یک کالکشن ۲ داکیومنت داریم که یکی زیر مجموعه‌ی دیگری هست درواقع بین این ۲ یک ارتباط وجود دارد

برای مثال میتواند یکی فرزند دیگری باشد. در این صورت زمانی که ما پدر رو میگیریم میخوایم فرزند هم همراه باهاش توی یک بخش دیتا به اسم children بیاد.

بعضی وقتا ما فقط نیاز به آیدی children داریم و بعضی وقتا ما نیاز به کل داکیومنت children داریم. در زمانی که ما نیاز به کل داکیومنت children داریم میتونیم یک virtual تعریف کنیم که هروقت پدر می‌اید فرزند هم همراهش بیاد طوری که تمام فرزند هاش توی یک عضو به اسم children باشن به صورت داکیومنتی

به طور خلاصه ما معمولا برای هر پدر آیدی فرزند هاشو قرار میدیم. بعضی وقتا میخوایم به جای آیدی کل اون داکیومنت مرتبط با اون آیدی رو به عنوان فرزند داشته باشیم در این صورت هنگام ساخت schema این کالکشن باید virtual تعریف کنیم و از طرفی هنگام گرفتن اون دیتا باید populate بکنیم

این تعریف لزوما برای ارتباط پدر و فرزندی نیست. میتونه برای زمانی باشه که ما نیاز داریم یک عضو sibling رو هم هنگامی که داکیومنت رو میگیریم همراه باهاش بگیریم

در این صورت تعریف virtual به شکل زیر هست:

SchemaName.virtual("return_name", {
ref: "schema name",
localField: "from_field",
foreignField: "to_field",
});

function autoPopulate() {
this.populate("return_name");
}

SchemaName.pre(["find", "findOne"], autoPopulate);

درواقع ما اینجا یک ارتباط بین from_field و to_field برقرار میکنیم و میگیم هرزمان که from_field رو گرفتیم مقادیری که to_field رو دارین رو کل داکیومنتتش رو بردار و بزار توی فیلدی به اسم return_name

info

از طرفی میگیم قبل از اعمال عملیات‌های find یا findOne میاد و populate میکنه. populate کردن هم به معنی قرار دادن to_field زیر مجموعه‌ی داکیومنت from_field با اسم return_name

Example

برای مثال ما یک دیتابیس برای کتگوری‌های سایتمون داریم که کتگوری میتونه یک فرزند از نوع کتگوری داشته باشه و فرزندش هم میتونه فرزند داشته باشه و ...

در این حالت ما آیدی پدر اصلی یک کتگوری رو نگه میداریم. در این صورت مونگوس خودش میفهمه به طور بازگشتی باید بره پدربزرگ پدر پدربزرگ و ... رو بگیره

حالا میخوایم وقتی یه پدری رو میگیریم تمام فرزند‌های هم توی یک فیلدی به اسم children توی json برگردونده بشه

در این صورت virtual زیر رو تعریف میکنیم

const { Schema, Types, model } = require("mongoose");

const CategorySchema = new Schema(
{
name: { type: String, required: true },
icon: { type: String, required: true },
slug: { type: String, required: true, unique: true, index: true },
parent: {
type: Types.ObjectId,
required: false,
default: null,
ref: "category",
},
},
{
toJSON: { virtuals: true },
id: false,
versionKey: false,
},
);
CategorySchema.virtual("children", {
ref: "category",
localField: "_id",
foreignField: "parent",
});

function autoPopulate() {
this.populate("children");
}

CategorySchema.pre(["find", "findOne"], autoPopulate);

const CategoryModel = model("category", CategorySchema);

module.exports = { CategoryModel };

درواقع ما در مثال بالا تعریف کردیم که یک فیلد parent به یک _id اشاره داره. از طرفی هروقت ما بخوایم یک پدر رو بگیریم تمام فرزند‌هاشو توی فیلدی به اسم children به صورت درختی به ما میده درصورت populate کردن

ما توی مثال بالا گفتیم که زماتی که عملیات find و findOne انجام میشه برو و populate بکن. این عمل به معنی قرار دادند فرزندهای یک کتگوری در قسمت children هست. یعنی فرزند‌های یک کتگوری رو همشون رو به صورت درختی در بخش children قرار میده

warning

درکل این مبحث توضیح دادنش خیلی سخته. ولی ایده به این صورت هست که هربار ما بخوایم اون localField رو بگیریم عملیات populate انجام میده و کل فرزند هاشو توی آبجکت به اسم children میریزه

تعریف هر بخش :

  • populate: میاد و دنبال ارتباط هایی که تعریف کردیم میگرده و توی فیلدی که میخوایم کل اون داکیومنت رو میریزه
  • SchemaName.pre() : یک تابع هست و قبل از انجام عملیات مدنظر کال میشه و میتونیم نوع عملیات هایی رو که میخوایم بهش بدیم
  • virtual :‌نوع روابط بین فیلد هارو مشخص میکنه که از کدوم به کدوم ارتباط داریم
  • ref: رفرنس میده که این فیلد به چه اسکیما(مدلی) ارتباط داره و حتمی چیزی که وارد میکنیم باید اسم یک اسکیما یا مدل باشه
  • localField: فیلد حال حاضر ما هست که براش ارتباط تعریف میکنیم
  • foreignField: میگه که این فیلد به کدوم فیلد ارتباط داره
  • درکل ما تعارف ارتباط هارو توی بخش virtual انجام میدیم

برای مثال اگر ما این دیتا هارو توی دیتابیس داشته باشیم :

database
[
{
"_id": "69b19ecda8f3b7dd36d69dd8",
"name": "ماشین",
"icon": "ICCar",
"slug": "cars",
"parent": null
},
{
"_id": "69b19f1490c9530a035bcb09",
"name": "ماشین برقی",
"icon": "ICCar",
"slug": "electric-car",
"parent": "69b19ecda8f3b7dd36d69dd8"
},
{
"_id": "69b1a3324bdfadaa046332f6",
"name": "ماشین کنترلی",
"icon": "ICCar",
"slug": "control-car",
"parent": "69b19f1490c9530a035bcb09"
},
{
"_id": "69b1a4fab22e0fbe09616892",
"name": "تلوزیون",
"icon": "ICTV",
"slug": "tv",
"parent": null
}
]

و روی اونا populate بزنیم این دیتا رو برمیگردونه :

result
[
{
"_id": "69b19ecda8f3b7dd36d69dd8",
"name": "ماشین",
"icon": "ICCar",
"slug": "cars",
"parent": null,
"children": [
{
"_id": "69b19f1490c9530a035bcb09",
"name": "ماشین برقی",
"icon": "ICCar",
"slug": "electric-car",
"parent": "69b19ecda8f3b7dd36d69dd8",
"children": [
{
"_id": "69b1a3324bdfadaa046332f6",
"name": "ماشین کنترلی",
"icon": "ICCar",
"slug": "control-car",
"parent": "69b19f1490c9530a035bcb09",
"children": []
}
]
}
]
},
{
"_id": "69b1a4fab22e0fbe09616892",
"name": "تلوزیون",
"icon": "ICTV",
"slug": "tv",
"parent": null,
"children": []
}
]

میبینیم که به صورت ساختار درختی برگردونه نتیجه رو

info

اگر دیتاهایی رو که پدر دارند رو ۲ بار بگردوند یبار تو ساختار درختی یبار هم جدا برگردوند بیا و موقع find کردن بگو فقط دیتا هایی رو برگردونه که parent اشون خالیه

await CategoryModel.find({ parent: null });
// --- OR ---
await CategoryModel.find({ parent: { $exist: false } });

Populate

یک تابعی هست که هنگام find یا findOne کردن در انتهاش اضافه میشه. به اینصورت هست که اگر یک فیلد از داکیومنت به یک داکیومنت دیگه از هر کالکشنی ارتباط(ref) داشته باشه وقتی find عادی میزنیم میره و صرفا مقدار ارتباط رو قرار میده اونجا

برای مثال اگر یک پروفایل به یک کاربر ارتباط داشته باشه userID -> _id وقتی ما یک پروفایل رو میگیریم جای userID صرفا یک آیدی میزاره. ولی وقتی تابع populate رو در انتهاش قرار بدیم کل اون داکیومنت رو برمیداره میزاره جاش. یعنی داکیومنتی که توی کالکشن یوزر بهش لینک شده رو کلشو برمیداره و میزاره جاش

await ModelName.find().populate("refrense name");
// ---OR---
await ModelName.findOne().populate("refrense name");

برای مثال اگر ما یک پروفایل داشته باشیم که متعلق به کاربر 69b2fa59acfc43b8c33a9e3e باشد درحالت عادی اگر فایند بزنیم روی این پروفایل به صورت زیر عمل میکند:

await ProfileModel.findOne({ _id: "69b30fe2ec80bd448125b417" });
result
{
"_id": "69b30fe2ec80bd448125b417",
"name": "amirhossein",
"family": "abazari",
"role": "admin",
"image": "docs/user.png",
"userID": "69b2fa59acfc43b8c33a9e3e",
"__v": 0
}

مشاهده میشه که توی بخش userID فقط آیدی مرتبط به اون یوزر رو گذاشته که توی کالکشن users وجود داره

حالا اگر populate رو بزنیم جای اینکه فقط userID رو بزاره کل داکیومنت رو برمیداره و جاش میزاره :

await ProfileModel.findOne({ _id: "69b30fe2ec80bd448125b417" }).populate(
"user",
);
{
"_id": "69b30fe2ec80bd448125b417",
"name": "amirhossein",
"family": "abazari",
"role": "admin",
"image": "docs/user.png",
"userID": {
"_id": "69b2fa59acfc43b8c33a9e3e",
"username": "am_abazari",
"password": "WsaHHI!@gb@B!2371tg!@###SJSaqsdasj",
"verified": true
},
"__v": 0
}

وقتی populate رو زدیم به جای اینکه فقط اون آیدی رو بزاره رفته و از کالکشن user یوزر مرتبط باهاش رو کل داکیومنتش رو گذاشته

info

کل منطق populate همینه که جای رفرنسی که دادی کل داکیومنت رو میره و پیدا میکنه میزاره جاش