MongoDB: نماذج مونغوس وتصميم المخطط
آخر تحديث: 2026-08-26
يُعد «Mongoose Schema» أكثر أدوات تصميم كائنات البيانات (ODM) الخاصة بـ MongoDB شيوعًا في منظومة Node.js — ويُعد إتقان تصميم «Schema» الأساس لبناء تطبيقات قوية.
تقدم هذه الدورة نظرة عامة منهجية على أنواع حقول مخطط Mongoose، والفرق بين النماذج والوثائق، والحقول الافتراضية، والبرمجيات الوسيطة.
ODM مقابل برنامج التشغيل: لماذا تختار Mongoose: هناك نهجان رئيسيان في نظام MongoDB Node.js — 1. برنامج التشغيل الأصلي (حزمة mongodb): خفيف الوزن ومرن وبدون تجريد؛ حيث يتعامل مباشرةً مع مستندات BSON وهو مناسب للسيناريوهات التي تتطلب أداءً وتحكمًا فائقين؛ 2. Mongoose (ODM): يوفر ميزات متقدمة مثل تعريف المخطط، والتحويل التلقائي للأنواع، وأدوات التحقق من الصحة، والبرمجيات الوسيطة، وpopulate، مما يجعله مناسبًا لتطوير تطبيقات الأعمال. الأسباب الرئيسية لاختيار Mongoose: 1. المخطط كوثيقة (أنواع الحقول والقيود ذاتية الوصف)؛ 2. التحقق التلقائي من الصحة يمنع البيانات غير الصالحة؛ 3. آلية البرمجيات الوسيطة تتعامل مع الشواغل المشتركة (تجزئة كلمات المرور، الحذف المؤقت، التسجيل)؛ 4. populate يحل محل $lookup لتبسيط استعلامات الانضمام. السيناريوهات التي يُفضل فيها استخدام برنامج التشغيل الأصلي: 1. السيناريوهات التي تعتمد على الأداء (تتسبب Mongoose في عبء إضافي بسبب التجريد)؛ 2. المخططات غير الثابتة (السجلات، بيانات إنترنت الأشياء)؛ 3. عندما يكون لديك بالفعل نظام التحقق من الصحة أو نظام البرمجيات الوسيطة الخاص بك.
1. ما ستتعلمه
- مخطط Mongoose: أكثر من 12 نوعًا من الحقول
- الفرق الجوهري بين النموذج والوثيقة
- افتراضي: حقل افتراضي (لا يتم حفظه في قاعدة البيانات ولكن يمكن الاستعلام عنه)
- البرامج الوسيطة (الخطافات قبل/بعد)
- طرق الكائنات والطرق الثابتة
- وراثة المخطط وآلية المكونات الإضافية
2. نظرة عامة على «البدء مع Mongoose»
mongoose = MongoDB + تحسينات ORM:
- يحدد المخطط هياكل البيانات (ما يعادل لغة تعريف البيانات (DDL))
- يوفر هذا النموذج واجهة برمجة تطبيقات (API) لعمليات CRUD
- توثيق طرق المثيل (الحفظ، التحقق من الصحة)
- تعمل البرمجيات الوسيطة على اعتراض الطلبات قبل العمليات وبعدها
طبقات التجريد الأساسية في Mongoose: يوفر Mongoose ثلاث طبقات من التجريد — 1. المخطط (تعريف البنية): يحدد أنواع الحقول وقواعد التحقق من الصحة والقيم الافتراضية والفهارس والحقول الافتراضية والبرمجيات الوسيطة؛ وهذا يعادل لغة DDL والقيود في SQL؛ 2. النموذج (عمليات المجموعة): يتم تجميعه من المخطط، ويوفر أساليب الفئة مثل البحث (find) والإنشاء (create) والتحديث (update) والحذف (delete)؛ وهذا يعادل واجهة CRUD في لغة SQL؛ 3. المستند (مثيل المستند): كائن مثيل تم إنشاؤه بواسطة النموذج، ويتميز بأساليب المثيل مثل save وvalidate وremove، بالإضافة إلى الحقول الافتراضية؛ وهذا يعادل سجلًا واحدًا في ORM. تتيح طبقات التجريد الثلاث هذه للمطورين التفاعل مع قواعد بيانات المستندات باستخدام نهج موجه للكائنات.
الاختيار بين Mongoose وبرامج التشغيل الأصلية: متى نستخدم Mongoose، ومتى نستخدم برنامج التشغيل الأصلي لـ MongoDB؟ يُعد Mongoose مناسبًا لما يلي: 1. منطق الأعمال المعقد (الذي يتطلب التحقق من الصحة، والبرمجيات الوسيطة، والحقول الافتراضية)؛ 2. التعاون بين أعضاء الفريق (نموذج «المخطط كوثيقة»، مع أمان الأنواع الذي يقلل من الأخطاء)؛ 3. هياكل البيانات المستقرة (تغييرات مخطط البيانات الخاضعة للرقابة). أما برنامج التشغيل الأصلي فهو مناسب لـ: 1. السعي لتحقيق أقصى أداء (تتسبب عملية تغليف المستندات في Mongoose في زيادة في الأداء بنسبة 10–20%)؛ 2. هياكل البيانات شديدة الديناميكية (يمكن أن تحد المخططات فعليًا من المرونة)؛ 3. عمليات القراءة والكتابة البسيطة (CRUD بدون التحقق من الصحة، والمعالجة المباشرة لـ BSON). بالنسبة لمعظم مشاريع Node.js، يعد استخدام Mongoose هو الخيار الصحيح — فكفاءة التطوير أهم من الاختلافات الطفيفة في الأداء.
const mongoose = require('mongoose');
// Connect
await mongoose.connect('mongodb://localhost:27017/shopdb');
// Definition Schema
const UserSchema = new mongoose.Schema({...});
// Create Model
const User = mongoose.model('User', UserSchema);
// Usage Model CRUD
const user = await User.create({...});
graph TB
A[mongoose Schema] --> B[SchemaType<br/>Field Type]
A --> C[Model<br/>Constructor]
A --> D[Document<br/>Examples]
A --> E[virtual<br/>Virtual Fields]
A --> F[middleware<br/>Middleware]
B --> B1[String/Number/Date]
B --> B2[ObjectId/Decimal128]
B --> B3[Mixed/Map/Array]
C --> C1[find/create]
D --> D1[save/validate]
F --> F1[pre/post hooks]
style C fill:#d4edda
style D fill:#cce5ff
3. أنواع حقول المخطط
شرح المفهوم: المخطط (schema) هو طبقة التعريف في Mongoose لهياكل مستندات MongoDB. وهو يحدد النوع وقواعد التحقق من الصحة والقيم الافتراضية واستراتيجيات الفهرسة لكل حقل. وعلى الرغم من أن MongoDB نفسها لا تعتمد على مخطط، فإن Mongoose تفرض تحديد الأنواع والتحقق من الصحة على مستوى طبقة التطبيق، مما يوفر لتطبيقات Node.js أمانًا للبيانات مشابهًا لتلك التي توفرها أدوات ORM التقليدية.
كيفية العمل: تحتفظ Mongoose Schema ببيانات تعريف الحقول (كائنات SchemaType) في ذاكرة التطبيق. عند إنشاء مستند أو تحديثه، تقوم Mongoose بتحويل الأنواع والتحقق من صحة البيانات حقلًا حقلًا؛ وستُحدث القيم التي لا تتوافق مع القواعد استثناءً ValidationError قبل save(). لا يؤثر المخطط على تخزين MongoDB — فعندما يفتقد مستند قديم إلى حقل جديد، يُرجع Mongoose undefined (والذي يمكن ملؤه بـ default).
السلوك الضمني للمخطط: يحتوي المخطط على العديد من السلوكيات الضمنية التي يسهل إغفالها — 1. تتضمن كل وثيقة تلقائيًا _id: ObjectId (حتى لو لم يتم الإعلان عنها في المخطط)؛ 2. بشكل افتراضي، لا يتم تضمين _id في ناتج toJSON (ما لم يتم تعيين toJSON: {virtuals: true})؛ 3. تحويل الأنواع ضمني — تمرير '123' إلى حقل Number يحوله تلقائيًا إلى 123 (في الوضع الصارم، strict: true)؛ وتمرير null إلى حقل يحتوي على required: true يؤدي إلى فشل التحقق من الصحة (null ≠ undefined، وrequired يتحقق فقط من undefined)؛ 4. يجب أن تُرجع الدالة القيم الافتراضية للكائنات المتداخلة (الافتراضي: () => ({})); وإلا، فستتشارك جميع المستندات نفس المرجع (وهو أحد المزالق الكلاسيكية في JavaScript).
graph LR
A[Schema Definition] --> B[SchemaType<br/>Field Metadata]
B --> C[Type Conversion<br/>String/Number/Date...]
B --> D[Validation Rules<br/>required/min/max/enum]
B --> E[Default value<br/>default/immutable]
B --> F[Indexing Strategies<br/>index/unique]
C --> G[Document.save]
D --> G
E --> G
G --> H{Verification Passed?}
H -->|Yes| I[MongoDB insertOne]
H -->|No| J[ValidationError]
style I fill:#d4edda
style J fill:#f8d7da
| تصنيف الأنواع | نوع المونغوس | السيناريوهات النموذجية |
|---|---|---|
| الأنواع الأساسية | سلسلة/رقم/منطقية/تاريخ | الاسم، السعر، زر التبديل، الطابع الزمني |
| ثنائي | مخزن مؤقت | الصور المصغرة، محتويات الملفات |
| المرجع | ObjectId + ref | علاقة المفتاح الخارجي (على سبيل المثال، categoryId → Category) |
| القيمة الدقيقة | العدد العشري 128 | المبلغ بالعملة (لتجنب أخطاء النقاط العائمة) |
| التداخل | تداخل المخططات | الهياكل الثابتة مثل العناوين والمواصفات |
| مصفوفة | [النوع] | قائمة بالعلامات، مجموعة من الصور |
| أخبار | مختلط/خريطة | بيانات وصفية ذات بنية غير مؤكدة، ترجمة متعددة اللغات |
(1) أكثر من 12 نوعًا من SchemaType
قرارات اختيار نوع المخطط (SchemaType): يعد اختيار نوع المخطط الصحيح الخطوة الأولى في نمذجة البيانات — فقد يؤدي الاختيار الخاطئ إلى مشكلات في جودة البيانات ومخاطر تتعلق بالأداء. المبادئ الأساسية: 1. يجب استخدام Decimal128 بدلاً من Number للمبالغ النقدية (لتجنب «فخ النقطة العائمة» حيث 0.1 + 0.2 ≠ 0.3)؛ 2. استخدم ObjectId + ref لعلاقات المفاتيح الخارجية بدلاً من String (تعتمد طريقة populate في Mongoose على ObjectId)؛ 3. استخدم Mixed للحقول ذات الهياكل غير المؤكدة بدلاً من Object (يسمح النوع Mixed بأي قيمة، بينما قد يؤدي النوع Object إلى حدوث تحقق من الصحة غير متوقع)؛ 4. استخدم Map بدلاً من Object للأعداد الكبيرة من أزواج المفاتيح والقيم (يمكن أن تكون مفاتيح Map من أي نوع وتدعم forEach وmap).
| النوع | تعريف Mongoose | نوع BSON | مثال |
|---|---|---|---|
| سلسلة | String |
سلسلة | String |
| الرقم | Number |
مزدوج | Number |
| منطقية | Boolean |
منطقية | Boolean |
| البيانات | Date |
البيانات | Date |
| المخزن المؤقت | Buffer |
ثنائي | Buffer |
| ObjectId | mongoose.Schema.Types.ObjectId |
ObjectId | ObjectId |
| Decimal128 | mongoose.Schema.Types.Decimal128 |
Decimal128 | Decimal128 |
| خريطة | Map |
الكائن | Map |
| المخطط | new mongoose.Schema({...}) |
كائن | مضمن |
| مصفوفة | [Type] |
مصفوفة | [String] |
| مختلط | mongoose.Schema.Types.Mixed |
كائن | Mixed |
مبادئ التصميم: تتبع تعريفات حقول المخطط فلسفة «القيود كوثيقة توضيحية» — فالخيارات المتاحة لكل حقل (مطلوب، الحد الأدنى، الحد الأقصى، قائمة، تطابق) ليست مجرد قواعد للتحقق من الصحة أثناء التشغيل فحسب، بل هي أيضًا إعلانات صريحة لعقد البيانات. وتجعل الحقول المحددة جيدًا المخطط نفسه معيارًا توثيقيًّا قابلاً للتنفيذ، مما يتيح لأعضاء الفريق فهم قيود كل حقل دون الحاجة إلى الرجوع إلى الويكي.
القرارات المعمارية: يتطلب اختيار خيارات الحقول تحقيق توازن بين الصرامة والمرونة. فقد تؤدي القيود المفرطة في الصرامة (مثل وجود عدد كبير جدًا من الحقول الإلزامية) إلى إعاقة قدرة النظام على التطور — لذا ينبغي أن تكون الحقول الجديدة اختيارية بشكل افتراضي، على ألا يتم تشديد القيود إلا بعد استقرار النظام. وعلى العكس من ذلك، تؤدي القيود المفرطة في التساهل إلى تراكم الديون التقنية. الاستراتيجية الموصى بها: تطبيق قيود صارمة على الحقول الأساسية للمعرفات (البريد الإلكتروني، رقم المنتج)؛ والتعامل مع الحقول الإضافية (الاسم المستعار، الصورة الرمزية) بمرونة أكبر؛ واستخدام القوائم المنسقة (enums) لتقييد حقول الحالة التجارية (الدور، الحالة).
(2) خيارات تعريف الحقول
const UserSchema = new mongoose.Schema({
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email'],
minlength: 5,
maxlength: 100,
index: true
},
age: {
type: Number,
required: true,
min: [0, 'Age cannot be negative'],
max: 150,
default: 18
},
role: {
type: String,
enum: {
values: ['customer', 'admin', 'moderator'],
message: 'Invalid role: {VALUE}'
},
default: 'customer'
},
isActive: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now,
immutable: true // Cannot be modified after creation
}
});
أفضل الممارسات: دليل لاختيار أنواع المخططات لبيئات الإنتاج — 1. استخدم دائمًا Decimal128 للمبالغ النقدية بدلاً من Number؛ فأخطاء النقاط العائمة غير مقبولة في الحسابات المالية؛ 2. استخدم ObjectId + ref للعلاقات المرجعية بدلاً من تضمين المستندات الكاملة لتجنب تكرار البيانات وتضارب التحديثات؛ 3. استخدم Mixed أو Map للبيانات الوصفية ذات الهياكل غير المؤكدة، ولكن كن على دراية بمخاطر فشل التحقق من الصحة؛ 4. استخدم دائمًا النوع Date مع timestamps: true لحقول التاريخ لتجنب الالتباس في المنطقة الزمنية مع التواريخ القائمة على السلاسل النصية.
تكوين خيارات المخطط: يتحكم كائن خيارات المخطط في Mongoose في السلوك العام — 1. timestamps: true: يدير حقلَي createdAt وupdatedAt تلقائيًّا؛ اضبط createdAt على immutable: true لمنع التعديلات غير المقصودة؛ 2. toJSON: {virtuals: true}: يتضمن التسلسل إلى JSON الحقول الافتراضية (لا يتم تضمينها افتراضيًّا)؛ 3. toJSON: {virtuals: true}: تتضمن toJSON() أيضًا الحقول الافتراضية؛ 4. minimize: false: لا تقم بضغط الكائنات الفارغة (بشكل افتراضي، تقوم Mongoose بإزالة الحقول الفارغة، مما قد يتسبب في فقدان الحقول المتوقعة في الواجهة الأمامية)؛ 5. strict: true: ترفض الحقول غير المُعرَّفة في المخطط (ممكّنة بشكل افتراضي؛ يجب أن تظل ممكّنة في بيئة الإنتاج). يجب تحديد هذه الخيارات في مرحلة مبكرة من المشروع، حيث إن التعديلات اللاحقة قد تؤثر على البيانات والسلوكيات الحالية.
المفاضلة بين التضمين والإحالة: هذا هو القرار الأكثر أهمية في تصميم مخطط MongoDB. يعمل التضمين على تخزين البيانات ذات الصلة داخل المستند نفسه، مما يسمح باسترداد جميع البيانات في استعلام واحد، لكنه يواجه مشكلات مثل تضخم المستند (الحد الأقصى 16 ميغابايت) وتعقيد عملية التحديث. أما الإحالة فتستخدم ObjectId لإنشاء العلاقات؛ وهي توفر استقلالية أكبر للبيانات ولا توجد قيود على الحجم، ولكنها تتطلب استعلامات populate/$lookup إضافية. معايير اتخاذ القرار: 1. هل تُقرأ البيانات معًا دائمًا؟ نعم → التضمين؛ 2. هل ستزداد البيانات المرتبطة بشكل غير محدود؟ نعم → الإحالة؛ 3. هل تحتاج البيانات المرتبطة إلى التحديث بشكل مستقل؟ نعم → الإحالة.
| البعد | التضمين | الإحالة |
|---|---|---|
| أداء الاستعلام | مرتفع (قراءة واحدة) | منخفض (يتطلب ملء البيانات) |
| اتساق البيانات | ضعيف (تحديثات متكررة) | قوي (تحديثات من نقطة واحدة) |
| حجم المستند | ⚠️ قد يتجاوز 16 ميغابايت | ✅ منفصل لكل مستند |
| حالة الاستخدام | 1:N — صغيرة وثابتة | 1:N — كبيرة أو متنامية |
▶ المثال 1: التطبيق العملي لأنواع المخططات المركبة
const ProductSchema = new mongoose.Schema({
// Basic Types
sku: { type: String, required: true, unique: true },
title: { type: String, required: true },
price: { type: mongoose.Schema.Types.Decimal128, required: true },
stock: { type: Number, default: 0, min: 0 },
isActive: { type: Boolean, default: true },
// Date
releaseDate: { type: Date, required: true },
expiryDate: { type: Date },
// Binary
thumbnail: { type: Buffer },
// Quote(Foreign Key)
categoryId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category',
required: true
},
// Nested Documents
specs: {
screen: String,
battery: String,
weight: Number
},
// Array
tags: [String],
images: [{
url: String,
alt: String
}],
// Map(Dynamic key-value pairs)
translations: {
type: Map,
of: String
},
// Mixed(Any type)
metadata: mongoose.Schema.Types.Mixed
}, { timestamps: true });
الإخراج:
TEXT 📖 للعرض فقطتم إنشاء مخطط المنتج بنجاح مع 12 نوعًا من الحقول، بما في ذلك الحقول الأساسية والتواريخ والثنائيات والمراجع والمستندات المتداخلة والمصفوفات والخرائط والحقول المختلطة.
4. النمذجة والتوثيق
شرح المفهوم: النموذج (Model) هو تجسيد مونغوس (Mongoose) لمجموعة MongoDB — وهو مُنشئ (فئة) يوفر طرقًا ثابتة مثل find وcreate وupdateOne. وDocument هو مثيل لـ Model يمثل سجلًا في قاعدة البيانات ويوفر طرقًا خاصة بالمثيل مثل save وvalidate وremove. وفهم الفرق بين Model وDocument هو الأساس لاستخدام Mongoose بشكل صحيح.
كيفية العمل: يقوم mongoose.model('User', schema) بأمرين: (1) ترجمة المخطط إلى منشئ نموذج؛ (2) تسجيله في اتصال Mongoose وربطه بمجموعة users (مع تحويل الاسم إلى صيغة الجمع تلقائيًّا). يقوم new User({...}) بإنشاء مثيل Document؛ وفي هذه المرحلة، تكون البيانات موجودة في الذاكرة فقط ولا تُكتب إلى MongoDB إلا عند استدعاء save(). User.create({...}) مكافئ لـ new User() + save().
sequenceDiagram
participant App as Application Code
participant Model as User Model
participant Doc as User Document
participant DB as MongoDB
App->>Model: User.create({email, age})
Model->>Doc: new User(data)
Doc->>Doc: validate()
Doc->>DB: insertOne()
DB-->>Doc: _id, createdAt
Doc-->>App: Back Document
App->>Model: User.find({role: 'admin'})
Model->>DB: find().toArray()
DB-->>Model: Array of original documents
Model->>Doc: hydrate(docs)
Doc-->>App: Document Array
style Model fill:#d4edda
style Doc fill:#cce5ff
(1) الاختلافات الرئيسية
| البعد | الطراز | المستند |
|---|---|---|
| الجوهر | المُنشئ (الفئة) | مثيل النموذج |
| إنشاء | mongoose.model('User', schema) |
new User({...}) أو User.create() |
| الكمية | 1 لكل مجموعة | 1 لكل وثيقة |
| الطريقة | الطرق الثابتة (find، create) | طرق المثيل (save، validate) |
مبادئ تصميم نظام الأنواع: يوفر نظام الأنواع في Mongoose أمانًا في الأنواع على مستوى طبقة التطبيق، وهو ما لا يتوفر أصلاً في MongoDB. يقوم الكائن SchemaType بإجراء تحويل الأنواع عند إنشاء المستند (على سبيل المثال، يتم تحويل السلسلة "42" تلقائيًّا إلى الرقم 42)؛ وإذا فشل التحويل، يتم إلقاء استثناء CastError. ورغم أن هذا التحويل الضمني مريح، إلا أنه قد يخفي أيضًا مشكلات في البيانات — في بيئات الإنتاج، يُوصى بتمكين الوضع الصارم strict: true في المخطط (وهو ممكّن افتراضيًا) لرفض الحقول غير المُعرَّفة.
مخاطر واحتياطات التحويل الضمني للأنواع: يُعد التحويل الضمني للأنواع في Mongoose سيفًا ذا حدين — 1. الراحة: عندما تمرر الواجهة الأمامية {age: "25"}، يتم تحويلها تلقائيًا إلى القيمة Number 25، وبالتالي لا يحتاج المطورون إلى إجراء التحويل يدويًّا؛ 2. المخاطر: تتحول "abc" إلى NaN (CastError) عند تحويلها إلى Number، ولكن إذا تم تعريف المخطط على أنه String وقام MongoDB بتخزين Number، فقد يعرض الاستعلام "غير موجود" (بسبب عدم تطابق الأنواع)؛ 3. استراتيجيات الوقاية: حدد الأنواع صراحةً في المخطط (لا تقم بتخزين أرقام حيثما يحدد المخطط نوع String)، وتحقق من صحة الأنواع في طبقة التطبيق (باستخدام التحقق من صحة joi قبل أن يقوم Mongoose بإجراء التحويل)، وقم بتنقية المدخلات في طبقة التوجيه (إزالة الحقول الزائدة). التحويل الضمني الأكثر خطورة: يُحدث الحقل ObjectId استثناءً من نوع CastError عند تلقيه سلسلة سداسية عشرية غير مكونة من 24 رقمًا (على سبيل المثال، تمرير القيمة «abc» إلى req.params.id)؛ يجب التحقق من صحة تنسيق ObjectId في طبقة التوجيه.
(2) طرق مثيلات المستندات
// === Create Document ===
const user = new User({ email: 'alice@example.com' });
// === Document Properties ===
user.email; // 'alice@example.com'
user._id; // ObjectId
user.createdAt; // Date
user.isNew; // true(Not saved)
// === Document Instance Methods ===
await user.save(); // Save
await user.validate(); // Verification(Do not save)
user.toJSON(); // Convert to JSON
user.toObject(); // Convert to a regular object
user.remove(); // Delete (Obsolete, use deleteOne)
await user.deleteOne(); // Delete(Recommendations)
await user.populate('orders'); // Associative Filling
▶ المثال 2: عمليات عملية على المستندات
إدارة دورة حياة المستندات: يمر المستند بأربع مراحل من الإنشاء إلى الإتلاف — 1. new User(data) ينشئ مثيلًا في الذاكرة (isNew: صحيح، لم يتم كتابته إلى قاعدة البيانات بعد)؛ 2. await user.save() يحفظ البيانات في MongoDB (مما يؤدي إلى تشغيل البرمجيات الوسيطة pre-save وعملية التحقق من الصحة)؛ 3. user.property = newValue تعديل خاصية في الذاكرة (مع تمييز الحقل المعدل بعلامة التتبع)؛ 4. await user.deleteOne() حذف المستند. الفرق الرئيسي: new + save عملية من خطوتين (يمكن تعديل البيانات قبل الحفظ)، بينما User.create() عملية من خطوة واحدة (تتم الكتابة مباشرةً إلى قاعدة البيانات).
// === Create and Save ===
const user = new User({
email: 'alice@example.com',
username: 'alice',
passwordHash: '...'
});
await user.save();
// === Save after making changes ===
user.lastLoginAt = new Date();
user.loginCount += 1;
await user.save();
// === Convert to an object ===
const userObj = user.toObject();
delete userObj.passwordHash;
// === populate Relationship ===
const user = await User.findById(userId).populate({
path: 'orders',
options: { sort: { createdAt: -1 } }
});
الإخراج:
TEXT 📖 للعرض فقط{ _id: ObjectId('...'), email: 'alice@example.com', username: 'alice', lastLoginAt: 2026-07-21T10:30:00.000Z, loginCount: 1 }
5. الحقول الافتراضية
شرح المفهوم: virtual هي آلية «الحقول المحسوبة» التي توفرها Mongoose — فهي تُعرِّف دالات الاسترجاع والتعيين في المخطط، لكنها لا تُحفظ في MongoDB. يتم حساب الحقول الافتراضية عند تحويل المستند بواسطة toJSON() أو toObject()، مما يجعلها مثالية للخصائص المشتقة (مثل fullName = firstName + lastName) وإحصائيات الارتباط (مثل orderCount).
كيفية العمل: «المُسترد الافتراضي» هو دالة تُجري عملية حسابية في كل مرة يتم فيها الوصول إلى doc.fullName. أما «المُعيّن الافتراضي» فيستقبل قيمة ويقسمها إلى حقول متعددة. والحقل الافتراضي العلائقي (ref + localField + foreignField) هو في الأساس إعلان مختصر لـ populate()؛ وعند الاستعلام عنه، لا يزال من الضروري استدعاء populate('orders') لتشغيل عملية ملء الارتباط.
graph TB
A[virtual Field] --> B[Computational<br/>fullName = firstName + lastName<br/>discountedPrice = price * 1-discount]
A --> C[Associative<br/>orders: ref Order<br/>orderCount: count true]
B --> D[Non-persistent<br/>In-Memory Computing]
C --> E[Requires populate<br/>Triggering a Joined Query]
D --> F[toJSONReal-time Output<br/>Needs to be set virtuals true]
E --> F
style D fill:#d4edda
style E fill:#cce5ff
| النوع الافتراضي | طريقة التعريف | هل هو ثابت؟ | هل يتطلب ملء البيانات؟ |
|---|---|---|---|
| مُسترد محسوب | schema.virtual('x').get(fn) |
لا | لا |
| مُعيّن حسابي | schema.virtual('x').set(fn) |
لا | لا |
| ترابطي (وثيقة) | schema.virtual('x', {ref, localField, foreignField}) |
لا | نعم |
| ترابطي (قابل للعد) | كما هو مذكور أعلاه + count: true |
لا | نعم |
// === Definition virtual ===
UserSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
UserSchema.virtual('isAdult').get(function() {
return this.age >= 18;
});
// === virtual setter(Reverse Settings)===
UserSchema.virtual('fullName').set(function(name) {
const parts = name.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
});
// === Enable virtual ===
UserSchema.set('toJSON', { virtuals: true });
UserSchema.set('toObject', { virtuals: true });
▶ المثال 3: الاستخدام العملي لـ virtual
خصائص أداء virtual: يتم حساب دالة الاسترجاع virtual (وليس تخزينها مؤقتًا) عند كل عملية وصول — فإذا تم تعديل حقل يعتمد عليه virtual، فإن عملية الوصول التالية ستُرجع القيمة الجديدة تلقائيًا. وهذا يعني: 1. في استعلامات القوائم، سيؤدي استدعاء حقل virtual في كل Document إلى تنفيذ دالة الحساب مرة واحدة؛ 2. لا يمكن استخدام حقول virtual في تصفية $match (لا يدرك MongoDB وجود حقول virtual)؛ 3. لا يمكن استخدام حقول virtual للفرز (كما هو مذكور أعلاه)؛ 4. عند تمكين virtuals: true في toJSON، يتم حساب جميع حقول virtual أثناء تسلسل JSON وإدراجها في الناتج.
الاختيار بين الحقول الافتراضية والحقول المحسوبة: متى تُستخدم الحقول الافتراضية ومتى تُخزَّن الحقول المحسوبة في المخطط — 1. تُعد الحقول الافتراضية مناسبة في الحالات التالية: عندما تعتمد نتيجة الحساب على حقول موجودة في المستند الحالي (على سبيل المثال، fullName = firstName + lastName)، ولا تكون النتيجة مطلوبة للاستعلامات أو الفرز أو عمليات التجميع، وتكون تكلفة الحساب منخفضة (مثل التسلسل البسيط للسلاسل النصية أو العمليات الحسابية)؛ 2. تكون الحقول المخزنة مناسبة في الحالات التالية: عندما تكون مطلوبة للاستعلامات أو الفرز (على سبيل المثال، discountedPrice تحتاج إلى الفرز حسب السعر المخفض)، أو عندما تكون تكلفة الحساب عالية (على سبيل المثال، عمليات التجميع عبر المجموعات)، أو عندما يكون الاستمرارية مطلوبة (على سبيل المثال، عمليات العد الزائدة مثل commentCount). مبدأ الاختيار — «إذا كان من الضروري استخدامه في استعلامات MongoDB، فيجب تخزينه؛ أما إذا كان يُعرض فقط في طبقة التطبيق، فإن استخدام حقل افتراضي يكون خيارًا أنظف».
// === Calculated Fields ===
ProductSchema.virtual('discountedPrice').get(function() {
if (!this.discount) return this.price;
return this.price * (1 - this.discount);
});
// === Related Fields(Non-persistent)===
UserSchema.virtual('orders', {
ref: 'Order',
localField: '_id',
foreignField: 'userId'
});
// Usage:
const user = await User.findById(userId).populate('orders');
console.log(user.orders); // Array of associated orders
// === Inverse Correlation ===
UserSchema.virtual('orderCount', {
ref: 'Order',
localField: '_id',
foreignField: 'userId',
count: true // Count only the number,Does not return a document
});
const user = await User.findById(userId).populate('orderCount');
console.log(user.orderCount); // 25
الإخراج:
TEXT 📖 للعرض فقط{ price: 100, discount: 0.2, discountedPrice: 80, orderCount: 25 }
6. البرمجيات الوسيطة
شرح المفهوم: برامج الوسيطة في Mongoose هي وظائف ربط يتم تنفيذها تلقائيًا قبل وبعد عمليات محددة في قاعدة البيانات. تُنفَّذ برامج الوسيطة «قبل» العملية (مثل تجزئة كلمة المرور، وتنقية البيانات)، بينما تُنفَّذ برامج الوسيطة «بعد» العملية (مثل تسجيل التدقيق، والإشعارات الفورية). وتُعد برامج الوسيطة أقوى آلية توسعة في Mongoose، حيث تتيح فصل المنطق التجاري عن عمليات البيانات.
كيفية العمل: تستخدم برمجيات Mongoose الوسيطة «نموذج البصل» — حيث يتم تنفيذ وظائف البرمجيات الوسيطة pre المتعددة بالترتيب الذي تم تسجيلها به، تليها العملية الفعلية، وأخيرًا يتم تنفيذ وظائف البرمجيات الوسيطة post بالترتيب الذي تم تسجيلها به. يجب أن تستدعي كل دالة من دوال البرمجيات الوسيطة pre دالة next() أو تُرجع وعدًا (Promise)؛ وإلا، يتم تعليق العملية. تتلقى دوال البرمجيات الوسيطة post نتيجة العملية كمعلمة ولا يمكنها تعديل سلوك العملية.
sequenceDiagram
participant App as Application Code
participant Pre1 as pre save #1<br/>Password Hash
participant Pre2 as pre save #2<br/>Email (lowercase)
participant DB as MongoDB
participant Post1 as post save #1<br/>Audit Log
participant Post2 as post save #2<br/>Welcome Email
App->>Pre1: doc.save()
Pre1->>Pre2: next()
Pre2->>DB: insertOne()
DB-->>Post1: Success
Post1->>Post2: next(doc)
Post2-->>App: Back doc
(1) أنواع البرمجيات الوسيطة
تصميم سلسلة تنفيذ البرامج الوسيطة: تستخدم البرامج الوسيطة في Mongoose نموذج «البصل» — حيث تنتقل الطلبات من الطبقة الخارجية إلى الطبقة الداخلية، بينما تنتقل الاستجابات من الطبقة الداخلية إلى الطبقة الخارجية. يتم تنفيذ خطافات pre بالتسلسل وفقًا لترتيب تسجيلها، ويجب أن تستدعي كل منها next() لتمرير التحكم؛ وبعد تنفيذ العملية الفعلية، يتم تنفيذ خطافات post وفقًا لترتيب تسجيلها. يفصل هذا التصميم بشكل طبيعي بين الاهتمامات: حيث يشغل كل من تجزئة كلمة المرور، وتنقية البيانات، وتسجيل التدقيق برمجيات وسيطة منفصلة، دون أي ارتباط بينها.
| النوع | شرط التشغيل | الغرض |
| pre('save') | قبل الحفظ | تجزئة كلمة المرور، الطابع الزمني |
| post('save') | بعد الحفظ | السجلات، الإشعارات |
| pre('validate') | التحقق المسبق | تنقية البيانات |
| pre('find') | قبل الاستعلام | معايير التصفية |
| pre('remove') | قبل الحذف | مسح البيانات المرتبطة |
(2) البرمجيات الوسيطة الخاصة بـ«pre-save»
أفضل الممارسات: الاستخدامات الثلاثة الأكثر شيوعًا للبرمجيات الوسيطة pre-save هي تجزئة كلمات المرور (باستخدام isModified لمنع التجزئة المكررة)، وتوحيد البيانات (تحويل عناوين البريد الإلكتروني إلى أحرف صغيرة، واقتطاع السلاسل)، وصيانة الطوابع الزمنية. النقطة الأساسية هي أن this تشير إلى مثيل Document الحالي، لذا لا يمكن تشغيل البرمجية الوسيطة pre-save أثناء العمليات الدفعية مثل Model.updateOne() — بالنسبة للعمليات الدفعية، استخدم البرمجية الوسيطة Query pre('updateOne') بدلاً من ذلك.
// === Password Hash(Classic Scenes)===
UserSchema.pre('save', async function(next) {
if (!this.isModified('passwordHash')) return next();
// The password has been changed.,Re-hash
this.passwordHash = await bcrypt.hash(this.passwordHash, 10);
next();
});
// === Timestamp ===
UserSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
(3) البرمجيات الوسيطة المسبقة البحث
برمجيات الوسيطة الخاصة بالاستعلام مقابل برمجيات الوسيطة الخاصة بالمستندات: pre find هي برمجيات وسيطة خاصة بالاستعلام (حيث يشير this إلى كائن الاستعلام وليس إلى كائن المستند)، وهي مناسبة للتحكم في سلوك الاستعلام العام — مثل التصفية التلقائية للمستندات المحذوفة، وتعبئة الارتباطات افتراضيًّا، والفرز افتراضيًّا. يتطابق تنسيق التعبير العادي pre(/^find/) مع جميع عمليات الاستعلام، بما في ذلك find وfindOne وfindById، مما يضمن اتساق السلوك. ملاحظة: لا يمكن لبرمجيات الوسيطة الخاصة بالاستعلام الوصول إلى بيانات المستند (لأن الاستعلام لم يتم تنفيذه بعد)؛ بل يمكنها فقط تعديل شروط الاستعلام.
// === Automatically filter deleted documents ===
UserSchema.pre(/^find/, function(next) {
this.find({ isDeleted: { $ne: true } });
next();
});
// === Automatic populate Relationship ===
UserSchema.pre('find', function(next) {
this.populate('categoryId');
next();
});
// === Default Sort Order ===
UserSchema.pre('find', function(next) {
this.sort({ createdAt: -1 });
next();
});
(4) البرمجيات الوسيطة «ما بعد الحفظ»
تصميم الآثار الجانبية في الوسيطة اللاحقة: تُنفَّذ الوسيطة اللاحقة بعد اكتمال العملية ولا يمكنها تعديل بيانات المستند (التي تم كتابتها بالفعل إلى قاعدة البيانات)؛ وهي مناسبة لتشغيل الآثار الجانبية — مثل سجلات التدقيق، والإشعارات الفورية، وتحديثات ذاكرة التخزين المؤقت. الميزات الرئيسية: 1. post_save تقبل معلمة doc (المستند المحفوظ)؛ 2. يحدد this.wasNew ما إذا كانت العملية إنشاءً أم تحديثًا؛ 3. يسترد this.modifiedPaths() قائمة بالحقول التي تم تعديلها؛ 4. تستخدم البرمجيات الوسيطة لمعالجة الأخطاء الإصدار ذي المعلمات الأربعة (err، doc، next)، وهي مصممة خصيصًا لالتقاط استثناءات العمليات.
// === Send the welcome email after saving ===
UserSchema.post('save', function(doc, next) {
if (this.wasNew) {
sendWelcomeEmail(doc.email);
}
next();
});
// === Record an audit log after saving ===
UserSchema.post('save', function(doc) {
AuditLog.create({
action: 'user.updated',
userId: doc._id,
changes: this.modifiedPaths()
});
});
▶ المثال 4: تدريب عملي على البرمجيات الوسيطة المتكاملة
// === User Schema Middleware ===
UserSchema.pre('save', async function(next) {
// 1. Password Hash
if (this.isModified('passwordHash')) {
this.passwordHash = await bcrypt.hash(this.passwordHash, 10);
}
// 2. Email (lowercase)
if (this.isModified('email')) {
this.email = this.email.toLowerCase();
}
next();
});
UserSchema.pre(/^find/, function(next) {
// By default, deleted users are not returned.
this.find({ isDeleted: { $ne: true } });
next();
});
// === Error-handling middleware ===
UserSchema.post('save', function(error, doc, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
next(new Error('Email already exists'));
} else {
next(error);
}
});
الإخراج:
TEXT 📖 للعرض فقطتم حفظ المستخدم بنجاح مع تجزئة كلمة المرور وتحويل البريد الإلكتروني إلى أحرف صغيرة. يتم استبعاد المستخدمين المحذوفين تلقائيًا من نتائج الاستعلام.
7. طرق المثيل والطرق الثابتة
شرح المفهوم: تتيح لك Mongoose تعريف نوعين من الطرق المخصصة في المخطط: الطرق الخاصة بالمثيل، والتي ترتبط بكل مستند على حدة وتعمل على المستندات الفردية (على سبيل المثال، user.comparePassword())؛ والطرق الثابتة، والتي ترتبط بالنموذج وتعمل على المجموعة بأكملها (على سبيل المثال، User.findByEmail()). يسمح هذان النوعان من الطرق بتغليف منطق الأعمال داخل نموذج البيانات، وفقًا لمبدأ تصميم «النموذج السمين».
كيفية العمل: يتم تعريف طرق المثيل عبر Schema.methods. عند new Model()، يقوم Mongoose بربط الطريقة بسلسلة نماذج Document، وداخل الطريقة، يشير this إلى Document الحالي. يتم تعريف الطرق الثابتة عبر Schema.statics وتركيبها على مُنشئ Model؛ وداخل الطريقة، يشير this إلى Model نفسه، مما يسمح بالاستدعاءات المباشرة لـ this.find() وهكذا دواليك.
graph TB
A[Schema Methods] --> B[Instance Methods<br/>Schema.methods]
A --> C[Static Methods<br/>Schema.statics]
B --> D["user.comparePassword(pwd)<br/>this = Currently Document"]
B --> E["user.generateToken()<br/>this = Currently Document"]
B --> F["user.softDelete()<br/>this = Currently Document"]
C --> G["User.findByEmail(email)<br/>this = User Model"]
C --> H["User.findActive()<br/>this = User Model"]
C --> I["User.getStatistics()<br/>this = User Model"]
style B fill:#cce5ff
style C fill:#d4edda
| معايير المقارنة | الطرق الخاصة بالمثيلات | الطرق الثابتة |
|---|---|---|
| تحديد الموقع | Schema.methods |
Schema.statics |
| كائن المرفق | نموذج الوثيقة | مُنشئ النموذج |
| يشير هذا إلى | المستند الحالي | النموذج نفسه |
| طريقة الاستدعاء | doc.method() |
Model.method() |
| الاستخدامات الشائعة | مقارنة كلمات المرور، الحذف المؤقت | عمليات البحث المشروطة، الإحصاءات التجميعية |
حدود تصميم الطرق: يستند التمييز بين الطرق الخاصة بالمثيلات والطرق الثابتة إلى مبدأ المسؤولية الواحدة — 1. الطرق الخاصة بالمثيلات: تعمل على بيانات المستند الواحد نفسه (على سبيل المثال، comparePassword لمقارنة كلمة المرور، وsoftDelete لوضع علامة الحذف، وtoJSON للإخراج المُجهَّل الهوية)، دون الاستعلام عن قاعدة البيانات (أو الاستعلام فقط عن البيانات المرتبطة بها)؛ 2. الطرق الثابتة: تعمل على البيانات على مستوى المجموعة (على سبيل المثال، findByEmail للاستعلامات عبر المستندات، وgetStatistics للإحصاءات المجمعة، وbulkImport للاستيراد بالجملة)، وتتطلب قدرات الاستعلام الخاصة بالنموذج. يؤدي طمس هذه الحدود إلى إرباك في التصميم — مثل وضع findByEmail في الطرق الخاصة بالمثيلات (نظرًا لأن المثيل يجب أن يكون موجودًا قبل إجراء البحث، مما يخلق تناقضًا منطقيًّا) أو وضع comparePassword في الطرق الثابتة (مما يتطلب تمرير كل من المستند وكلمة المرور، مما يجعلها زائدة عن الحاجة).
(1) طرق الكائنات
// === Define an instance method ===
UserSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.passwordHash);
};
UserSchema.methods.generateAuthToken = function() {
return jwt.sign({ id: this._id }, process.env.JWT_SECRET, { expiresIn: '7d' });
};
// === Usage ===
const user = await User.findOne({ email: 'alice@example.com' });
const isValid = await user.comparePassword('password123');
const token = user.generateAuthToken();
(2) الطرق الثابتة
// === Defining Static Methods ===
UserSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email.toLowerCase() });
};
UserSchema.statics.findActive = function() {
return this.find({ isActive: true });
};
// === Usage ===
const user = await User.findByEmail('ALICE@example.com');
const activeUsers = await User.findActive();
▶ المثال 5: تطبيق النهج المتكامل عمليًّا
النماذج الموسعة مقابل النماذج المبسطة: تدعو Mongoose إلى تصميم «النموذج الموسع» — حيث يتم تغليف المنطق التجاري داخل أساليب النموذج/الوثيقة، بحيث تكتفي وحدات التحكم باستدعاء user.comparePassword() بدلاً من تنفيذ bcrypt.compare بنفسها. مزايا النماذج السميكة: 1. إعادة استخدام الكود (تتشارك عدة وحدات تحكم في نفس الأسلوب)؛ 2. التغليف (الكود الخارجي لا يدرك كيفية التحقق من صحة كلمات المرور)؛ 3. قابلية الاختبار (يمكن اختبار أساليب النموذج بشكل مستقل). أما في حالة النماذج الرقيقة، فإن وحدات التحكم تمتلئ بالكود المكرر؛ حيث يتطلب تغيير قاعدة تحقق واحدة تعديل N من وحدات التحكم.
// === Complete User Model ===
const UserSchema = new mongoose.Schema({...});
// Instance Methods
UserSchema.methods = {
comparePassword: async function(candidate) {
return await bcrypt.compare(candidate, this.passwordHash);
},
softDelete: async function() {
this.isDeleted = true;
this.deletedAt = new Date();
return await this.save();
}
};
// Static Methods
UserSchema.statics = {
findByEmail: function(email) {
return this.findOne({ email: email.toLowerCase() });
},
getStatistics: async function() {
return await this.aggregate([
{ $group: { _id: '$role', count: { $sum: 1 } } }
]);
}
};
الإخراج:
TEXT 📖 للعرض فقط{ _id: 'admin', count: 5 } { _id: 'customer', count: 120 } { _id: 'moderator', count: 12 }
❓ أسئلة شائعة
virtuals: true في toJSON حتى يتم عرضها في واجهة برمجة التطبيقات (API).async function أو قم بإرجاع Promise. يجب عليك استدعاء next() أو إرجاع Promise؛ وإلا فسيتم تعليق العملية.pre next(error) وفي المرحلة post next(error) إلى آلية معالجة الأخطاء في Mongoose.discriminators (المُميِّز): const AdminUser = User.discriminator('admin', AdminSchema)، حيث تشترك جميع المُميِّزات في نفس المجموعة.📖 ملخص
- أنواع المخططات في Mongoose 12+: String/Number/Date/ObjectId/Decimal128/Map/Mixed
Modelهي مُنشئ (فئة)، وDocumentهي مثيل- لا يتم حفظ الحقول الافتراضية، ولكن يمكن إرجاعها باستخدام toJSON
- البرامج الوسيطة: قبل (قبل العملية) + بعد (بعد العملية)
- طرق المثيل: خاصة بكل مستند (مثل comparePassword)
- الطرق الثابتة: خاصة بكل نموذج (على سبيل المثال، findByEmail)
📝 تمارين
- تمرين أساسي (⭐): حدد مخطط «المستخدم» (بما في ذلك البريد الإلكتروني واسم المستخدم والعمر والدور)، وأنشئ نموذج «المستخدم».
- السؤال الأساسي (⭐): استخدم
virtualلتحديد حقلfullName(firstName+lastName). - تمرين متقدم (⭐⭐): قم بتنفيذ عملية تجزئة كلمة المرور (التحقق من isModified) باستخدام البرمجيات الوسيطة
pre-save. - مشكلة متقدمة (⭐⭐): استخدم البرمجية الوسيطة
pre_findلتصفية المستخدمين المحذوفين تلقائيًا. - التحدي (⭐⭐⭐): قم بتنفيذ نموذج مستخدم كامل (بما في ذلك تجزئة كلمة المرور، والحقول الافتراضية، وطرق المثيل، والطرق الثابتة) يدعم ميزات التسجيل، وتسجيل الدخول، والحذف المؤقت، وإعداد التقارير.