Kodax

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();
}
اختبر فهمك