نموذج بياناتنا يخدم سيدين: كيف أنقذنا نمط CQRS من جحيم تضارب القراءة والكتابة؟

يا جماعة الخير، السلام عليكم ورحمة الله.

أذكرها وكأنها البارحة، كانت ليلة من ليالي “الجمعة البيضاء” قبل كم سنة. كنا في “غرفة الحرب” الافتراضية، أنا وفريق التطوير، نراقب لوحات التحكم والعيون شابحة على الشاشات. الأرقام كانت بتصعد بشكل جنوني، عدد المستخدمين، الطلبات في الدقيقة، كل شيء كان “مولّع”. وفجأة، بدأت التنبيهات تصرخ زي المجنونة: “High CPU Usage”, “Database Connection Timeout”, “Request Timed Out”. الموقع صار أبطأ من سلحفاة مصابة بالزكام، والمستخدمون بدأوا يشكون على وسائل التواصل الاجتماعي.

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

ما هو أصل المشكلة؟ نموذج البيانات “أبو وجهين”

في معظم التطبيقات التقليدية، نبدأ بنموذج واحد لكل شيء. لو عنا “منتج” (Product)، فبكون عنا كائن واحد أو جدول واحد في قاعدة البيانات اسمه `Product`. هذا الكائن المسكين مسؤول عن كل شيء:

  • عمليات الكتابة (Writes): تحديث السعر، تغيير الكمية، إضافة منتج جديد. هذه العمليات تتطلب قواعد عمل (Business Rules) معقدة، وتحقق من الصلاحيات (Validation)، واتساق في البيانات (Consistency).
  • عمليات القراءة (Reads): عرض قائمة المنتجات في الصفحة الرئيسية، البحث عن منتج، عرض تفاصيل منتج. هذه العمليات تتطلب سرعة فائقة، وتحسينات (Optimizations) مختلفة تمامًا، وغالبًا ما تحتاج لعرض بيانات من عدة جداول معًا.

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

الحل السحري (أو بالأحرى، المنطقي): نمط CQRS

وهنا يأتي دور المنقذ، نمط تصميم معماري اسمه CQRS. الاسم الطويل بخوّف شوي، لكن الفكرة بسيطة وعبقرية.

CQRS هو اختصار لـ Command Query Responsibility Segregation، ويعني ببساطة: “فصل مسؤولية الأوامر عن الاستعلامات”.

ايش هو الـ CQRS أصلاً؟

الفكرة هي أن لا نستخدم نفس النموذج العقلي (Mental Model) ونفس الكائنات البرمجية (Objects) لعمليات تعديل البيانات (الكتابة) وعمليات قراءة البيانات. بدلًا من ذلك، نقوم بفصلهم تمامًا:

  • جانب الأوامر (Command Side): مسؤول فقط عن تنفيذ الأوامر التي تغير حالة النظام. مثل `CreateProductCommand` أو `UpdateStockCommand`. هذا الجانب يركز على الاتساق وقواعد العمل.
  • جانب الاستعلامات (Query Side): مسؤول فقط عن جلب البيانات لعرضها. مثل `GetProductsForHomePageQuery` أو `GetProductDetailsQuery`. هذا الجانب يركز على السرعة والأداء.

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

الأوامر (Commands) مقابل الاستعلامات (Queries)

عشان نوضح الصورة أكثر:

  • الأمر (Command): هو طلب لتنفيذ مهمة ما. عادة لا يُرجع بيانات، وإنما فقط يعلمك بنجاح أو فشل المهمة. هو فعل مُوجّه: “أضف هذا المنتج”، “احذف هذا المستخدم”.
  • الاستعلام (Query): هو سؤال عن حالة النظام الحالية. لا يغير أي شيء في النظام إطلاقًا. هو طلب للمعلومات: “أعطني كل المنتجات تحت سعر 50 دولار”، “ما هو اسم هذا المستخدم؟”.

هذا الفصل هو جوهر القوة في CQRS.

كيف طبقنا الـ CQRS على أرض الواقع؟

الحكي سهل، لكن كيف طبقنا هذا الكلام فعلًا؟ رح أمشي معكم خطوة بخطوة في رحلتنا.

الخطوة الأولى: فصل النماذج (Models) في الكود

أول شيء عملناه كان داخل الكود نفسه. قبل ما نغير أي شيء في قاعدة البيانات. كان عنا كائن `Product` معقد جدًا:


// قبل CQRS: كائن واحد لكل شيء
public class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }

    // خصائص للعرض فقط
    public string FormattedPrice { get; set; } // مثل "$99.99"
    public bool HasLowStockWarning { get; set; }

    // دوال للكتابة مع قواعد عمل
    public void ChangePrice(decimal newPrice) { /* ... منطق معقد ... */ }
    public void DecreaseStock(int quantity) { /* ... منطق معقد ... */ }
}

أول خطوة كانت فصل هذا الكائن إلى نموذجين:

  1. نموذج للكتابة (Write Model): هذا هو الـ Aggregate Root في عالم الـ Domain-Driven Design. يركز فقط على حماية قواعد العمل.
  2. نموذج للقراءة (Read Model): هذا كائن بسيط (DTO – Data Transfer Object) مُحسّن لغرض عرض معين.

// بعد CQRS: فصل النماذج

// 1. نموذج الكتابة (Aggregate)
public class Product
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public decimal Price { get; private set; }
    public int StockQuantity { get; private set; }

    // ... دوال لتغيير الحالة فقط، مثل ChangePrice, DecreaseStock ...
    // لا يوجد أي خصائص خاصة بالعرض هنا
}

// 2. نموذج القراءة (Read Model / DTO)
public class ProductViewModel
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string FormattedPrice { get; set; }
    public bool IsAvailable { get; set; }
    public string ImageUrl { get; set; }
    // ... أي شيء نحتاجه للعرض، حتى لو كان من جداول أخرى
}

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

الخطوة الثانية: فصل قواعد البيانات (اختياري ولكنه قوي جدًا)

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

  • قاعدة بيانات الكتابة (Write DB): حافظنا عليها كقاعدة بيانات علائقية (SQL Server). فهي ممتازة في فرض القيود، وضمان الاتساق (ACID transactions). هذا هو “مصدر الحقيقة” (Source of Truth) لدينا.
  • قاعدة بيانات القراءة (Read DB): هنا كانت المتعة. استخدمنا قاعدة بيانات NoSQL (في حالتنا كانت Elasticsearch) مُحسّنة للبحث والقراءة السريعة. قمنا بعمل “Denormalization” للبيانات. يعني، بدل ما نعمل `JOIN` بين 5 جداول عشان نجيب تفاصيل المنتج، صرنا نخزن “وثيقة” (Document) واحدة لكل منتج فيها كل البيانات اللي بحتاجها للعرض.

هذا سمح لنا بتحسين كل قاعدة بيانات للمهمة المخصصة لها. قاعدة بيانات الكتابة مُحسّنة للـ Transactions، وقاعدة بيانات القراءة مُحسّنة للسرعة الفائقة.

الخطوة الثالثة: المزامنة بين العالمين (Synchronization)

هنا السؤال اللي بسأل مليون دولار: إذا غيرنا شيء في قاعدة بيانات الكتابة، كيف منحدّث قاعدة بيانات القراءة؟

هذه هي أهم نقطة، وهناك عدة طرق للتعامل معها. الطريقة التي اتبعناها، والتي تعتبر الشريك الطبيعي للـ CQRS، هي استخدام نمط الـ Event Sourcing مع ناقل رسائل (Message Bus).

العملية تسير كالتالي:

  1. عندما يصل أمر جديد (مثل `UpdatePriceCommand`)، يقوم جانب الكتابة بتنفيذه.
  2. بدلًا من مجرد تحديث الحقل في قاعدة البيانات، يقوم النظام بإنشاء “حدث” (Event) يصف ما حصل. مثل `ProductPriceChangedEvent`.
  3. يتم حفظ هذا الحدث في “متجر الأحداث” (Event Store)، وهو سجل دائم لكل التغييرات التي حدثت في النظام.
  4. يتم نشر هذا الحدث على ناقل الرسائل (مثل RabbitMQ أو Azure Service Bus).
  5. هناك “مستمع” (Listener) أو أكثر يهمه هذا الحدث. أحد هؤلاء المستمعين هو المسؤول عن تحديث قاعدة بيانات القراءة.
  6. عندما يستلم المستمع حدث `ProductPriceChangedEvent`، يقوم بتحديث وثيقة المنتج المقابلة في قاعدة بيانات القراءة (Elasticsearch) بالسعر الجديد.

بهذه الطريقة، تصبح قاعدة بيانات القراءة “متسقة في النهاية” (Eventually Consistent) مع قاعدة بيانات الكتابة. وهذا يعني أنه قد يكون هناك تأخير بسيط جدًا (أجزاء من الثانية) بين حدوث التغيير وظهوره للمستخدم. لكن لمعظم التطبيقات، هذا مقبول تمامًا مقابل الأداء الهائل الذي نكسبه.

نصائح من “الختيار” (أبو عمر)

بعد ما خضنا هذه التجربة، تعلمت كم شغلة بحب أشاركها معكم من القلب:

  • مش كل إشي بده CQRS: يا جماعة، الحكي إلك يا كنة واسمعي يا جارة. لا تستخدم CQRS لكل مشروع صغير أو تطبيق بسيط. هو نمط قوي لكنه يضيف تعقيدًا. استخدمه فقط في أجزاء النظام التي تعاني فعلًا من تضارب القراءة/الكتابة، أو عندما تكون لديك قواعد عمل معقدة جدًا.
  • الاتساق النهائي (Eventual Consistency) صديقك، بس افهمه صح: فكرة أن البيانات قد لا تكون محدّثة 100% في نفس اللحظة قد تكون مخيفة. لكن في 95% من الحالات، هي مقبولة تمامًا. هل يهم حقًا إذا رأى المستخدم السعر القديم لجزء من الثانية قبل أن يتحدث؟ في معظم الأحيان، لا. لكن في حالات مثل التأكد من كمية المخزون قبل إتمام الدفع، يجب أن تعود دائمًا إلى “مصدر الحقيقة” (Write Model).
  • ابدأ بسيطًا: لست مضطرًا لتطبيق كل شيء مرة واحدة. يمكنك البدء بتطبيق CQRS داخل نفس التطبيق ونفس قاعدة البيانات، فقط عن طريق فصل الكود إلى مسارات للأوامر ومسارات للاستعلامات. هذا بحد ذاته سيحقق لك “شغل مرتب” ووضوحًا كبيرًا. ثم، عندما تحتاج، يمكنك فصل قواعد البيانات.

الخلاصة: من الفوضى إلى النظام 🧘‍♂️

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

بفصلنا بين السيدين المتناقضين – القراءة والكتابة – تمكنا أخيرًا من خدمة كل منهما بأفضل طريقة ممكنة. حصلنا على نظام متين وموثوق لعمليات الكتابة، ونظام فائق السرعة ومرن لعمليات القراءة. لم نعد نخاف من مواسم الضغط، بل صرنا ننتظرها لنرى نظامنا وهو يتعامل معها بكل أريحية.

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

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

ذكاء اصطناعي

كانت نماذجنا تهذي بلا توقف: كيف أنقذنا ‘التوليد المعزز بالاسترجاع’ (RAG) من جحيم الهلوسات؟

في أحد المشاريع، كاد نموذج الذكاء الاصطناعي أن "يخرب بيتنا" بهلوساته وإجاباته الخاطئة. هذه المقالة تروي قصة كيف أنقذتنا تقنية التوليد المعزز بالاسترجاع (RAG)، وتشرح...

31 مايو، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

كنا نبني جدرانًا رقمية: كيف فتحت لنا ‘إمكانية الوصول’ (Accessibility) أبوابًا لم نكن نراها؟

اعتقدنا أننا نبني تطبيقات رائعة، لكننا كنا في الحقيقة نبني جدرانًا رقمية. في هذه المقالة، يشارك أبو عمر كيف غيّر فهم 'إمكانية الوصول' (Accessibility) منظوره...

30 مايو، 2026 قراءة المزيد
برمجة وقواعد بيانات

كانت صفحاتنا تموت من ألف استعلام: كيف أنقذتنا تقنيات ‘التحميل المسبق’ (Eager Loading) من جحيم مشكلة N+1؟

أشارككم قصة حقيقية من أرض المعركة البرمجية، كيف اكتشفنا عدوًا صامتًا يسمى "مشكلة N+1" كان يقتل أداء تطبيقنا، وكيف كانت تقنية التحميل المسبق (Eager Loading)...

30 مايو، 2026 قراءة المزيد
الحوسبة السحابية

كانت بيئاتنا جزرًا من الفوضى: كيف أنقذتنا “البنية التحتية كشفرة” (IaC) من جحيم الانحراف التكويني؟

أشارككم قصة من قلب الميدان، عن ليلة كادت أن تنهار فيها أنظمتنا بسبب تغيير يدوي بسيط. سأشرح لكم كيف كانت "البنية التحتية كشفرة" (IaC) وأدوات...

30 مايو، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

مقابلاتي التقنية كانت كارثة: كيف أنقذني ‘التفكير بصوت عالٍ’ من جحيم الفشل؟

أشارككم قصة شخصية عن فشلي في المقابلات التقنية بسبب الصمت القاتل، وكيف غيرت استراتيجية "التفكير بصوت عالٍ" مساري المهني. اكتشفوا معي كيف تحولون المقابلة من...

30 مايو، 2026 قراءة المزيد
البودكاست