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 میسازیم
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 یا فایل اصلی پروژه برای اتصال به دیتابیس استفاده میکنیم
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 مرتبط به یوزرها رو تعریف میکنیم
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 ما به صورت زیر عمل میکنیم :
const result = await UserModel.create({
username: "amirhossein",
password: "iran12Sa@!3",
});
همچنین با insertOne هم میشه انجام داد
const result = await UserModel.insertOne({
username: "amirhossein",
password: "iran12Sa@!3",
});
مقدار 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 میتونیم از کد زیر استفاده کنیم
const result = await UserModel.insertMany([
{
username: "amirhossein",
password: "password1",
},
{
username: "aliakbar",
password: "password2",
},
]);
مقدار 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 رو بخوام باید بزنم :
const result = await UserModel.find({}, { username: true });
و مقدار result برابر زیر هست:
[
{
"_id": "69ad61b013e6bde575e146f0",
"username": "amabazari"
},
{
"_id": "69ad6e7913b9d8b9c2953343",
"username": "amabazari2"
}
]
نکته: همواره مقدار _id برگردونده میشود
Lean Document
به طور پیشفرض مقداری که برگردونده میشه شامل عبارتهای زیادی هست اگر ما هنگام find کردن یک داکیومنت به انتهای اون متد lean رو کال کنیم پرفورمنس MongoDB بالا میره و بهتر میتونه عمل بکنه و چیزایی که نیاز نداریم رو حذف میکنه
await ModelName.findOne(condition).lean();
توی این حالت خیلی پرفورمنس بالاتر میره
برای مثال برای پیدا کردن یک یوزر با موبایلش به صورت زیر عمل میکنیم
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
از طرفی میگیم قبل از اعمال عملیاتهای 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 قرار میده
درکل این مبحث توضیح دادنش خیلی سخته. ولی ایده به این صورت هست که هربار ما بخوایم اون localField رو بگیریم عملیات populate انجام میده و کل فرزند هاشو توی آبجکت به اسم children میریزه
تعریف هر بخش :
- populate: میاد و دنبال ارتباط هایی که تعریف کردیم میگرده و توی فیلدی که میخوایم کل اون داکیومنت رو میریزه
- SchemaName.pre() : یک تابع هست و قبل از انجام عملیات مدنظر کال میشه و میتونیم نوع عملیات هایی رو که میخوایم بهش بدیم
- virtual :نوع روابط بین فیلد هارو مشخص میکنه که از کدوم به کدوم ارتباط داریم
- ref: رفرنس میده که این فیلد به چه اسکیما(مدلی) ارتباط داره و حتمی چیزی که وارد میکنیم باید اسم یک اسکیما یا مدل باشه
- localField: فیلد حال حاضر ما هست که براش ارتباط تعریف میکنیم
- foreignField: میگه که این فیلد به کدوم فیلد ارتباط داره
- درکل ما تعارف ارتباط هارو توی بخش virtual انجام میدیم
برای مثال اگر ما این دیتا هارو توی دیتابیس داشته باشیم :
[
{
"_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 بزنیم این دیتا رو برمیگردونه :
[
{
"_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": []
}
]
میبینیم که به صورت ساختار درختی برگردونه نتیجه رو
اگر دیتاهایی رو که پدر دارند رو ۲ بار بگردوند یبار تو ساختار درختی یبار هم جدا برگردوند بیا و موقع 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" });
{
"_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 یوزر مرتبط باهاش رو کل داکیومنتش رو گذاشته
کل منطق populate همینه که جای رفرنسی که دادی کل داکیومنت رو میره و پیدا میکنه میزاره جاش