Relationships و Population — MongoDB
العلاقات: التضمين والمراجع
في SQL القرار واضح: جداول منفصلة ومفاتيح أجنبية. في MongoDB تختار بين تضمين البيانات داخل المستند أو الإشارة إليها في مجموعة أخرى — والقرار يعتمد على كيف تقرأ البيانات.
التضمين (Embedding)
const orderDoc = {
_id: "o1",
customer: "u1",
items: [ // مضمنة: تُقرأ دائماً مع الطلب
{ productId: "p1", name: "قلم", price: 1.5, qty: 3 },
{ productId: "p2", name: "دفتر", price: 3.0, qty: 1 },
],
shippingAddress: { city: "بغداد", street: "شارع فلسطين" },
};
console.log(orderDoc.items.length, "عناصر في قراءة واحدة");✅ قراءة واحدة، وتحديث ذري للمستند كله.
❌ حد المستند 16MB، والتكرار (اسم المنتج منسوخ في كل طلب).
لاحظ: نسخ اسم المنتج وسعره داخل الطلب صحيح هنا — الطلب يجب أن يحفظ السعر وقت الشراء حتى لو تغيّر لاحقاً.
المراجع (References)
import mongoose from "mongoose";
const postSchema = new mongoose.Schema({
title: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, index: true },
});
// const posts = await Post.find().populate("author", "name"); // يجلب المؤلف من مجموعة users✅ لا تكرار، والعلاقات الكبيرة أو غير المحدودة ممكنة.
❌ قراءات إضافية؛ و populate ليس JOIN: إنه استعلام ثانٍ من التطبيق.
كيف تقرر؟
| السؤال | تضمين | مرجع |
|---|---|---|
| هل تُقرأ البيانات دائماً معاً؟ | ✅ | |
| هل لها معنى مستقل وتُقرأ وحدها؟ | | ✅ |
| كم عدد العناصر؟ | قليلة ومحدودة (عناوين المستخدم) | كثيرة أو غير محدودة (تعليقات منشور مشهور، طلبات عميل) |
| هل تتغير كثيراً وتُشارك بين مستندات؟ | | ✅ |
قاعدة شائعة: "واحد لقليل" ← تضمين، "واحد لكثير جداً" ← مرجع في جهة الكثير (comment.post)، لا مصفوفة متضخمة في المنشور.
متعدد لمتعدد
طلاب ↔ دورات: مصفوفة معرّفات في جهة واحدة إن كانت صغيرة (student.courses)، أو مجموعة وسيطة enrollments (كالجدول الوسيط في SQL) إن احتاجت بيانات إضافية (تاريخ التسجيل، الدرجة).
populate بحكمة
- حدد الحقول: .populate("author", "name avatar") — لا تجلب passwordHash بالخطأ.
- populate داخل حلقة = مشكلة N+1؛ populate لقائمة واحدة يجمع المعرّفات في استعلام واحد بـ $in.
- للتقارير المعقدة: $lookup في Aggregation (الدرس التالي).
فهرس على حقول المراجع
author: { ..., index: true } — بدونه Post.find({ author: id }) يمسح المجموعة كلها.
تمرين
صمم مخططات لمتجر: منتجات، عملاء، طلبات (بعناصر)، ومراجعات المنتجات. لكل علاقة قرر تضميناً أم مرجعاً وبرّر قرارك بطريقة القراءة المتوقعة.
// npm install mongoose
import mongoose from "mongoose";
await mongoose.connect(process.env.MONGODB_URI ?? "mongodb://127.0.0.1:27017/kodax_demo");
mongoose.set("sanitizeFilter", true); // يحيّد عوامل $ القادمة من مدخلات المستخدم
const { Schema } = mongoose;
const User = mongoose.models.User4 ?? mongoose.model("User4", new Schema({ name: String, email: String }));
// مرجع: المنشورات كثيرة وتُقرأ وحدها — كل منشور يشير لمؤلفه
const Post = mongoose.models.Post4 ?? mongoose.model("Post4", new Schema({
title: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: "User4", required: true, index: true },
}));
// تضمين: عناصر الطلب وعنوانه تُقرأ دائماً معه، والسعر يُحفظ وقت الشراء
const Order = mongoose.models.Order4 ?? mongoose.model("Order4", new Schema({
customer: { type: Schema.Types.ObjectId, ref: "User4", required: true, index: true },
items: [{ name: String, price: Number, qty: { type: Number, min: 1 } }],
shippingAddress: { city: String, street: String },
}));
try {
const sara = await User.create({ name: "سارة", email: "sara@example.com" });
await Post.create([{ title: "أول منشور", author: sara._id }, { title: "عن MongoDB", author: sara._id }]);
const posts = await Post.find({ author: sara._id }).populate("author", "name").lean();
console.log(posts.map((p) => `${p.title} — ${p.author.name}`));
const order = await Order.create({
customer: sara._id,
items: [{ name: "قلم", price: 1.5, qty: 3 }, { name: "دفتر", price: 3, qty: 1 }],
shippingAddress: { city: "بغداد", street: "شارع فلسطين" },
});
const total = order.items.reduce((sum, i) => sum + i.price * i.qty, 0);
console.log("مجموع الطلب:", total);
} finally {
await mongoose.disconnect();
}اختبر فهمك