كل الأعمال

دراسة حالة · SaaS متعدد المستأجرين

Food Link — قاعدة بيانات لكل تاجر

منصة طلبات متعددة المستأجرين للمطاعم والمتاجر، لا يمكن فيها إنشاء تاجر جديد داخل transaction واحدة.

المشكلة

تبيع Food Link خدمة الطلب عبر الإنترنت للمطاعم والمتاجر بنظام الاشتراك. يحصل كل تاجر على واجهة متجر وحسابات للموظفين وكتالوج وعملاء لهم محافظ. والمتطلب الذي حدّد شكل كل شيء آخر: بيانات كل تاجر يجب أن تبقى معزولة عن بيانات أي تاجر آخر.

دوري

كنت مطوّر الـ Backend الوحيد منذ أن كان الـ repository فارغًا. تغطي دراسة الحالة هذه نواة المنصة التي بنيتها: تعدد المستأجرين وتجهيز التاجر (provisioning)، والاشتراكات، و APIs لوحة تحكم الإدارة ولوحة تحكم التاجر، وإعدادات المتجر، والكتالوج، والعملاء والمحافظ، وتذاكر الدعم، وسجل التدقيق، والتصدير. أما واجهة المتجر الخاصة بالعميل والطلبات فكانت المرحلة التالية.

المعمارية

المعمارية modular monolith من 18 module. قاعدة بيانات مركزية واحدة تحمل بيانات المنصة نفسها (التجار والباقات والاشتراكات). ولكل تاجر قاعدة بيانات خاصة به.

  1. Route
  2. FormRequest
  3. DTO
  4. Service
  5. Repository
  6. Resource
كل endpoint يمر بالمسار نفسه.

الـ modules لا يستورد أحدها من الآخر. تتواصل عبر عقود (contracts) موجودة في module اسمه Core، ولكل عقد تنفيذ فارغ (null implementation)، فيمكن إيقاف أي module دون أن يتعطل الباقي. ووثّقت القرارات الرئيسية في 24 سجل قرار معماري (architecture decision record).

القرارات الأساسية

قاعدة بيانات لكل تاجر، لا عمود tenant_id. اخترت ذلك لسببين:

  • العزل. إذا نُسي شرط where فلا يمكن أن يتسرّب عملاء تاجر آخر، لأن صفوفهم ليست في قاعدة البيانات التي يتصل بها الطلب.
  • خروج نظيف. التاجر الذي يكبر حتى تضيق به المنصة يستطيع أن يغادر بنطاقه الخاص وبياناته ونسخة مرخّصة من الكود. ومع قاعدة بيانات لكل تاجر، المطلوب تسليم قاعدة بيانات واحدة، دون صفوف تحتاج إلى فصلها عن جداول مشتركة.

مكان واحد يقرّر ما يُسمح به للتاجر. الاشتراك يضع التاجر في واحدة من أربع حالات: محظور، أو قيد الإعداد، أو وصول كامل، أو قراءة فقط. service واحدة تحدّد الحالة، والـ middleware يفرضها. وإذا تعذّر تحديد الحالة، تُرفض عمليات الكتابة.

قواعد تفرضها الاختبارات لا العُرف. 107 اختبارات معمارية تُفشل الـ build إذا اعتمد module مباشرةً على ما بداخل module آخر، أو إذا استُخدم model خاص بالتاجر على الاتصال المركزي.

المال بأعداد عشرية دقيقة. تُحسب الأسعار حتى أربع خانات عشرية، ويُجرى التقريب في موضع واحد فقط. محفظة العميل ledger يقبل الإضافة فقط (append-only): كل حركة تأخذ lock على صف المحفظة وتسجّل الرصيد قبلها وبعدها.

الجزء الصعب: إنشاء تاجر

تسجيل تاجر جديد يعني الكتابة في قاعدة البيانات المركزية وإنشاء قاعدة بيانات جديدة. في MySQL ينفّذ الأمر CREATE DATABASE عملية commit ضمنية، فلا يمكن وضعه داخل transaction ليشمله الـ rollback مع بقية الخطوات. وإذا فشلت خطوة لاحقة، يبقى تاجر نصف مُنشأ.

بنيت التجهيز على شكل سلسلة خطوات، كل خطوة فيها تعرف كيف تلغي ما فعلته.

  1. إنشاء سجلات التاجر والنطاق

    في قاعدة البيانات المركزية، داخل transaction واحدة.

  2. إنشاء قاعدة بيانات التاجر

    خارج أي transaction، لأن الأمر ينفّذ commit من تلقاء نفسه.

    إذا فشلت: تُحذف السجلات المركزية.

  3. منح الصلاحيات لمستخدم قاعدة البيانات وضبط ترميز الأحرف (character set)

    إذا فشلت: تُحذف قاعدة البيانات، ثم تُحذف السجلات.

  4. تشغيل الـ migrations وإنشاء حساب المالك

    داخل قاعدة البيانات الجديدة.

    إذا فشلت: تُحذف قاعدة البيانات، ثم تُحذف السجلات.

  5. تعيين حالة التاجر إلى «جاهز»

لكل خطوة بعد الأولى إجراء تعويضي يُنفَّذ إذا فشلت خطوة لاحقة.

كان هناك قيد من الاستضافة أيضًا. خوادم الشركة تعمل على cPanel، وفيه لا يستطيع التطبيق ببساطة أن ينشئ قواعد بيانات بمستخدم MySQL ذي صلاحيات عالية. كتبت مدير قواعد بيانات (database manager) يمر عبر الـ API الخاص بـ cPanel لإنشاء قاعدة البيانات ومنح الصلاحيات للمستخدم وضبط ترميز الأحرف. قد يبلّغ cPanel عن فشل مع أن التنفيذ ينتهي برمز نجاح، لذلك يقرأ المدير محتوى الاستجابة (response body) ولا يثق برمز الخروج (exit code).

النتيجة

  • سُلّمت نواة المنصة في نحو سبعة أسابيع: 18 module ونحو 250 مسارًا في الـ API.
  • يغطيها نحو 2,900 دالة اختبار، تشمل عزل المستأجرين، ووضع القراءة فقط، والتجهيز، ومسارات الفشل في cPanel.
  • المعمارية موثّقة في 24 سجل قرار، فتنتقل أسباب القرارات مع الكود.

ما الذي سأفعله بعد ذلك

  • تشغيل مجموعة الاختبارات على MySQL في الـ CI. الاختبارات تستخدم حاليًا SQLite في الذاكرة، وهو لا يختبر الـ row locks كما تعمل في بيئة الإنتاج.
  • إضافة تحليل ساكن للكود (static analysis)، مع pipeline يشغّل الاختبارات عند كل merge request.
  • إضافة اختبار للـ rollback الكامل للتجهيز، إلى جانب الاختبارات الموجودة لكل خطوة.

السيرة الذاتية

تحميل PDF
صفحة السيرة الذاتية 1صفحة السيرة الذاتية 2