تكلمت في مقالة سابقة بعنوان لماذا نود ومونغو، عن الحوافز التي قد تدفع بأحدهم إلى استخدام مونغو عوض قواعد البيانات العلائقية التقليدية. وقد أوضحت كيف أن نظام قاعدة البيانات مونغو هو بلا شيمة (Schema-less). في هذه المقالة سأعطي أمثلة تطبيقية لكيف يمكن نمذجة وإنجاز العلاقات ما بين الوحدات في قواعد البيانات مونغو، وسأستخدم لأجل ذلك حزمة مونغوس Mongoose.

اصطلاحات

بالنسبة لذوي الخلفيات العلائقية، أسرد هنا المصطلحات التقليدية ومقابلاتها في مونغو:

اصطلاح سكيولاصطلاح مونغوMongo TermSQL Term
قاعدة بياناتقاعدة بياناتData BaseData Base
جدولمجموعةCollectionTable
سطروثيقةDocumentRow
عمودحقلFieldColumn

ما هو مونغوس؟

مونغوس هو مكتبة نمذجة بيانات كائنية لمونغو ونود Nodejs، وهي تسمح بتحديد العلاقات ما بين البيانات، وتعطي وسائل لوصف الشيمة (مخطط قاعدة البيانات) والتحقق من صحة البيانات، والتحويل ما بين بيانات مونغو والكائنات على مستوى الرماز (الكود). إذا كان يتبادر إليك السؤال هل يجدر بك استخدام مكتبة مونغوس أو استخدام مونغو مباشرة فجوابي لك هو إذا كنت مبتدئًا مع مونغو ومازلت في طور التأقلم معه فاستخدم مونغوس، حتى تضبط مفاهيمه وتصبح قادرًا على التفريق اللاواعي وغير المتأثر بين طريقة مونغو والطريقة العلائقية التقليدية حينئذ يمكنك استخدام مونغو مباشرة دون مكتبة.

بناء الشيمة

أعلم أنّ schema يقابلها في العربية مخطط. فكان لزامًا عليّ أن أقول بناء المخطط (مخطط قاعدة البيانات). لكنّي أجد أن مصطلح شيمة يشبه المصطلح الإنجليزي المتداول بين المبرمجين، وبالتالي فهو مألوفٌ وأكثر اعتيادية، كما أنّه عربي ولو كان له معنى آخر لكن لا ضير في اقتراضه واستعماله في نطاق المعلوماتيات بمعنى مختلف.

الهدف من هذه المقالة إظهار طرق نمذجة العلاقات، لذلك لن أخوض كثيرًا في شرح كيفية وصف الشيمة. سوف نأخذ كمثال الوحدات التالية: متجر، إعدادات، طلبية. حيث أن الوحدة متجر تمثل متجرًا إلكترونيا، والوحدة طلبية تمثل عملية مبيع على متجر إلكتروني، والوحدة إعدادات تمثل مجموعة إعدادات لمتجر إلكتروني.

لنبدأ بوصف شيمة المتجر.

const mongoose = require('mongoose');

const storeSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true
    },
    url: {
        type: String,
        required: true
    },
    platform: {
        type: String,
        enum: ['Shopify', 'WooCommerce'],
        required: true
    },
    access_token: {
        type: String
    },
    created_at: {
        type: Date,
        required: true,
        default: Date.now
    }
})

module.exports = mongoose.model('Store', storeSchema)

والآن وصف شيمة الطلبية:

const mongoose = require('mongoose');

const orderSchema = new mongoose.Schema({
    order_id: {
        type: Number,
        required: true
    },
    status: {
        type: String,
        enum: ['Processing', 'Ignored', 'Saved'],
        required: true,
        default: 'Processing'
    },
    tracking_info: {
        type: String
    },
    carrier_name: {
        type: String
    },
    created_at: {
        type: Date,
        required: true,
        default: Date.now
    }
})

module.exports = mongoose.model('Order', orderSchema);

وأخيرًا وصف شيمة الإعدادات:

const settingsSchema = new mongoose.Schema({
    type: {
        type: String,
        required: true
    },
    value: {
        type: String
    }
})

لقد قمنا إلى حد الآن بوصف الشيمات فقط ولم نضع أي اعتبار للعلاقات فيما بينها. تعطي مونغوس نوعان من علاقات النماذج: النماذج المضمّنة، النماذج العادية.

النماذج العادية

النماذج العادية Normalized Models، هي الأقرب نوعًا ما من المفهوم العلائقي التقليدي، وفيها يتم تحديد العلاقات ما بين النماذج باستخدام المراجع References.

فإذا أخذنا النموذجين متجر وطلبية، فإن هناك علاقة ما بينهما من نوع: كثير لواحد. أي للمتجر الواحد طلبيات كثيرة، وسوف نقوم بتمثيلها باستخدام النماذج العادية، أي أننا سنضع مرجع المتجر في الطلبية يتمثل في معرّف المتجر، من أجل ذلك سوف نعدّل شيمة الطلبية أعلاه لندرج مرجعًا إلى المتجر:

store: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Store',
    required: true
}

يمكننا الآن إنشاء طلبية جديدة مع تحديد متجرها، ولاختبار ذلك نرسل بوست POST مع عيّنة البيانات التالية:

{
  "tracking_info": "12ZAZ",
  "order_id": 2345,
  "carrier_name": "La Poste",
  "store": "5e74eb3f2466a42fceea074d"
}

إذا أعطينا معرّف متجر غير موجود فإنّ عملية التحقق التلقائية التي يضمنها مونغوس سوف تردّ خطأ مفاده أن المتجر المحدد للطلبية غير موجود. الآن إذا قمنا بطلب قيت GET للبيانات فسنحصل على النتيجة التالية:

{
    "sync_status": "Processing",
    "_id": "5e75206f907c1c60d5b8e373",
    "order_id": 2345,
    "tracking_info": "12ZAZ",
    "carrier_name": "La Poste",
    "store": "5e74eb3f2466a42fceea074d",
    "created_at": "2020-03-20T19:58:39.587Z",
    "__v": 0
}

هذه نتيجة طيّبة لكن ليست هي المرغوبة، فنحن نريد كائن متجر store يحمل في طيّاته جميع حقول المتجر وليس فقط معرّفه، من أجل ذلك فإننا نعدّل على مستوى الرماز ونستخدم الوظيفة populate()، لاحظ أن هذه المقالة ليست لشرح رمازات العمل وإنما نمذجة الشيمات فقط، وأنا أعتبر أن القارئ على دراية كاملة بكيفية تأسيس رمازات (أكواد) التعامل مع هذه الشيمات وتلقّي وردّ طلبات أشتتبي والتواصل مع قاعدة البيانات. وهذه وظيفة توجيهة قيت للطلبيات GET route function:

// Get all orders
router.get('/', async (req, res) => {
    try {
        const orders = await Order.find().populate('store')
        res.json(orders)
    }catch (err) {
        res.status(500).json({ message: err.message })
    }
})

الآن تصبح النتيجة كالتالي:

{
    "sync_status": "Processing",
    "_id": "5e75206f907c1c60d5b8e373",
    "order_id": 2345,
    "tracking_info": "12ZAZ",
    "carrier_name": "La Poste",
    "store": {
        "_id": "5e74eb3f2466a42fceea074d",
        "name": "Mysotre",
        "url": "mystore.myshopify.com",
        "platform": "Shopify",
        "created_at": "2020-03-20T16:11:43.296Z",
        "__v": 0
    },
    "created_at": "2020-03-20T19:58:39.587Z",
    "__v": 0
}

عظيم، فقط باستخدام populate تكفّل مونغوس بجلب الكائن متجر كاملًا ضمن كائن الطلبية.

النماذج المضمّنة

مفهوم النماذج المضمّنة Embedded Models، هو جديد في عالم مونغو أو قواعد البيانات الكائنية بصفة عامة، وغير موجود في الأسلوب العلائقي. ومفاده أنك تقوم بتضمين شيمة (أ) داخل شيمة (ب)، وبالتالي فإن الشيمة الفرعية (أ) لا توجد ولا تُقرأ إلا في سياق الشيمة الأب (ب). وعودة إلى مثالنا، لدينا نموذج الإعدادات الذي يرتبط ارتباطًا ضمنيا مع نموذج المتجر، إذ أن الإعدادات هي جزء من المتجر ولا معنى لها خارج سياق المتجر، كما أننا لن نحتاج إلى الوصول إلى الإعدادات بشكل منفرد دون المرور عبر المتجر وذلك على عكس الطلبيات إذ قد نرغب باستعلام طلبية ما بمعرّفها بغض النظر عن المتجر الذي تنتمي إليه. في حالة كهذه، يكون من الأفضل اعتماد نموذج مضمّن وذلك بتعديل شيمة المتجر وإضافة حقل الإعدادات لتصبح كالتالي:

const storeSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true
    },
    url: {
        type: String,
        required: true
    },
    platform: {
        type: String,
        enum: ['Shopify', 'WooCommerce'],
        required: true
    },
    access_token: {
        type: String
    },
    created_at: {
        type: Date,
        required: true,
        default: Date.now
    },
    settings: [settingsSchema]
})

لقد أضفنا الحقل settings وجعلناه مصفوفة لـ settingsSchema التي سبق أن عرّفناها.

لإضافة متجر جديد فنحن نستطيع إرسال إعداداته معه وهذا مثال لطلب بوست:

{
    "name": "MyStore",
    "url": "mystore2.myshopify.com",
    "platform": "Shopify",
    "settings": [
      {
        "type": "Paypal_id",
        "value": "MyPaypalID"
      },
      {
        "type": "Paypal_secret",
        "value": "MyPaypalSecret"
      }
     ]
}

يعطي النتيجة التالية:

{
    "_id": "5e75384087563c7db18c5343",
    "name": "Mystore",
    "url": "mystore2.myshopify.com",
    "platform": "Shopify",
    "settings": [{
        "_id": "5e75384087563c7db18c5344",
        "type": "Paypal_id",
        "value": "MyPaypalID"
    }, {
        "_id": "5e75384087563c7db18c5345",
        "type": "Paypal_secret",
        "value": "MyPaypalSecret"
    }],
    "created_at": "2020-03-20T21:40:16.880Z",
    "__v": 0
}

طيّب جميل، ماذا لو أردنا إضافة إعدادات إلى متجر موجود مسبقًا دون إنشائه؟ في هذه الحالة سوف نضيف توجيهة route جديدة خاصة بإضافة الإعدادات للمتاجر، ونقوم فيها بدمج صفيفة الإعدادات الجديدة مع صفيفة إعدادات المتجر الموجودة، ثم نحفظ التعديلات، وهذا مثال لوظيفة التوجيهة route function.

// Add settings to store
router.post('/:id/settings', getStore, async (req, res) => {
    const store = res.store
    const settings = req.body.settings
    store.settings = store.settings.concat(settings)
    try {
        const updatedStore = await store.save()
        res.json(updatedStore) 
    } catch {
        res.status(400).json({ message: err.message })
    }
})

لاحظ أنني لا أشرح الأرمزة سطرًا سطرًا لأن ذلك خارج سياق المقالة، وأنا أفترض أن القارئ له إلمام بإطار العمل إكسبرس Express وقاعدة البيانات مونغو.

خاتمة

أعطيت في هذه المقالة طُرق نمذجة وإنجاز العلاقات بين النماذج (الوحدات) في مونغو، الطريقة الأولى هي النموذج العادي وهي تعبّر عن العلاقات “الخفيفة” التي يكون فيها للوحدتين استقلالية ذاتية بحيث نستطيع استعلام والتعامل مع كل وحدة بشكل شبه منفصل، حينئذ نكتفي بحفظ مرجع يتمثل في معرّف النموذج المرتبط. والطريقة الثانية هي النموذج المضمّن وهي تعبّر عن العلاقات “القوية” حيث تحتوي الوحدة الأب الوحدة الابن، ولا يمكننا التعامل مع الوحدة الابن خارج سياق الوحدة الأب.

لديك سؤال؟ لا تتردد في التعليقات، سأسعد بالمساعدة.