كان نموذج بياناتنا وحشاً متعدد الرؤوس: كيف روّضناه بنمط CQRS و Event Sourcing؟

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

بتذكر قبل كم سنة، كنا شغالين على نظام كبير ومعقد، نظام فيه من كل بحر قطرة: إدارة مستخدمين، طلبات، مخزون، تقارير، ولوحة تحكم تحليلية. في البداية، الأمور كانت “آخر حلاوة”. كنا مبسوطين على قاعدة البيانات العلائقية الموحدة تبعتنا، وكل شيء كان يمشي زي الساعة.

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

لماذا كان نموذج بياناتنا وحشًا متعدد الرؤوس؟

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

تعقيد القراءة والكتابة

عندما تصمم نموذجًا للكتابة، أنت تركز على “التطبيع” (Normalization) لضمان سلامة البيانات وتجنب التكرار. لكن هذا النموذج المُطبّع يصبح كابوسًا عند القراءة، حيث تحتاج إلى عمل `JOIN` بين عدد كبير من الجداول لجلب معلومة بسيطة، وهذا بطيء ومكلف.

بالمقابل، لو صممت نموذجًا للقراءة، ستركز على “اللامعيارية” (Denormalization) لتجميع كل البيانات التي تحتاجها شاشة معينة في مكان واحد، مما يجعل القراءة سريعة جدًا. لكن هذا النموذج سيجعل عمليات الكتابة والتحديث جحيمًا، لأنك ستحتاج لتحديث نفس المعلومة في أماكن متعددة.

باختصار، كنا نحاول نجبر نموذج واحد على أن يكون جيدًا في شيئين متضادين تمامًا. زي ما بنحكي، “صاحب بالين كذاب”.

مشاكل الأداء والتوسع

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

الحل الأول: فصل المهام بنمط CQRS

بعد بحث وقراءة، وجدنا ضالتنا في نمط معماري اسمه CQRS، وهو اختصار لـ Command and Query Responsibility Segregation. الاسم طويل ومعقد، لكن الفكرة بسيطة بشكل عبقري.

ما هو CQRS ببساطة؟

الفكرة هي أن تفصل بشكل كامل بين المسار الذي يغير بيانات النظام (الكتابة) والمسار الذي يقرأ بيانات النظام (القراءة).

  • الـ Command (الأمر): هو كائن يمثل نية لتغيير شيء ما في النظام. مثلاً: `CreateOrderCommand` أو `UpdateProductPriceCommand`. الأوامر لا تُرجع بيانات، هي فقط تنفذ عملية.
  • الـ Query (الاستعلام): هو طلب للحصول على بيانات. مثلاً: `GetOrderDetailsQuery` أو `GetAllProductsQuery`. الاستعلامات لا تغير أي شيء في النظام أبدًا، هي فقط تقرأ.

هذا الفصل يسمح لنا ببناء نموذجين مختلفين تمامًا: نموذج للكتابة (Write Model) وآخر للقراءة (Read Model).

كيف طبقنا CQRS؟

قمنا بتقسيم الكود إلى مسارين واضحين:

  1. مسار الأوامر (The Write Side):
    • استخدمنا نموذج بيانات مُطبّع (Normalized) وغني بالمنطق البرمجي (Rich Domain Model).
    • ركزنا على مفاهيم الـ Domain-Driven Design (DDD) مثل الـ Aggregates لضمان تناسق البيانات وصحة قواعد البزنس.
    • كل أمر كان يتم معالجته بواسطة Handler خاص به، يقوم بالتحقق من الصلاحيات، وتطبيق قواعد البزنس، ثم تغيير حالة النظام.
  2. مسار الاستعلامات (The Read Side):
    • أنشأنا نماذج بيانات منفصلة ومُحسّنة لكل شاشة أو تقرير. هذه النماذج كانت بسيطة جدًا وغير مُطبّعة (Denormalized).
    • على سبيل المثال، كان لدينا جدول اسمه `OrderSummary` يحتوي على كل البيانات التي تحتاجها شاشة قائمة الطلبات، جاهزة للعرض بدون أي `JOIN`.
    • الاستعلامات كانت بسيطة ومباشرة، تقرأ من هذه الجداول المُحسّنة بسرعة فائقة.

مثال بالكود (شبه كود):


// Command Side
public class PlaceOrderCommand {
    public Guid CustomerId { get; set; }
    public List<OrderItem> Items { get; set; }
}

public class PlaceOrderCommandHandler {
    public void Handle(PlaceOrderCommand command) {
        // 1. Validate the command
        // 2. Check inventory
        // 3. Calculate total price
        // 4. Save the order to the "Write" database
        // 5. ...
    }
}

// Query Side
public class OrderSummaryQuery {
    public Guid CustomerId { get; set; }
}

public class OrderSummaryQueryHandler {
    public List<OrderSummaryDto> Handle(OrderSummaryQuery query) {
        // Directly query the "Read" model (e.g., a simple table)
        // SELECT * FROM OrderSummaries WHERE CustomerId = @CustomerId
        // This is super fast!
        return readDb.Query<OrderSummaryDto>(...);
    }
}

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

الترقية: Event Sourcing لتسجيل كل همسة ولمسة

تخيل معي لو أن قاعدة بياناتك لا تخزن الحالة النهائية للبيانات، بل تخزن كل تغيير حدث على هذه البيانات منذ نشأتها كسلسلة من الأحداث (Events). هذا هو جوهر الـ Event Sourcing.

ما هو الـ Event Sourcing؟

بدلًا من تخزين سجل “الطلب” في جدول الطلبات، نقوم بتخزين سلسلة من الأحداث التي أدت إلى تكوين هذا الطلب. مثلاً:

  1. `OrderPlaced` (تم إنشاء الطلب)
  2. `PaymentInfoAdded` (تم إضافة معلومات الدفع)
  3. `OrderConfirmed` (تم تأكيد الطلب)
  4. `OrderShipped` (تم شحن الطلب)

هذه الأحداث هي “مصدر الحقيقة” الوحيد (Single Source of Truth). الحالة الحالية لأي طلب هي ببساطة نتيجة تطبيق كل هذه الأحداث بالترتيب. إذا أردت معرفة حالة الطلب الآن، قم بتشغيل كل أحداثه من البداية للنهاية.

مزايا لا تقدر بثمن

  • سجل تدقيق كامل (Full Audit Trail): أنت تملك تاريخًا كاملاً وغير قابل للتغيير لكل ما حدث في النظام. هذا كنز للمحللين، لخدمة العملاء، وللمبرمجين عند تتبع الأخطاء.
  • استعلامات زمنية (Temporal Queries): يمكنك بسهولة الإجابة على أسئلة مثل: “كيف كان شكل هذا الطلب الأسبوع الماضي قبل أن يتم تعديله؟”. هذا شبه مستحيل في الأنظمة التقليدية.
  • مرونة هائلة في نماذج القراءة: هل تريد تقريرًا جديدًا لم تفكر به من قبل؟ لا مشكلة! أنشئ نموذج قراءة جديد، وقم “بإسقاط” (Project) كل الأحداث من البداية عليه لتعبئته بالبيانات. لست مقيدًا بالماضي.

CQRS + Event Sourcing: الثنائي الذي لا يقهر

عندما ندمج النمطين معًا، نحصل على نظام قوي ومرن بشكل لا يصدق. إليك كيف يعمل التدفق:

  1. مستخدم يرسل `Command` (مثلاً، `PlaceOrderCommand`).
  2. الـ `CommandHandler` يستقبل الأمر، يقوم بالتحقق من المنطق، ويقرر إنشاء حدث `OrderPlaced`.
  3. هذا الحدث يتم حفظه بشكل دائم في “متجر الأحداث” (Event Store)، وهو قاعدة بيانات مُحسّنة لتخزين الأحداث.
  4. بمجرد حفظ الحدث، يتم نشره عبر ناقل رسائل (Message Bus).
  5. “المُسقِطات” (Projectors) تستمع لهذه الأحداث. كل مُسقِط مسؤول عن تحديث نموذج قراءة معين.
  6. مثلاً، لدينا `OrderSummaryProjector` يستمع لحدث `OrderPlaced` ويضيف سجلًا جديدًا في جدول `OrderSummaries` في قاعدة بيانات القراءة.
  7. عندما يطلب المستخدم عرض قائمة طلباته، يتم إرسال `Query` يقرأ مباشرة من جدول `OrderSummaries` السريع والمُحسّن.

بهذه الطريقة، أصبح نموذج الكتابة (Event Store) منفصلاً تمامًا عن نماذج القراءة، والتزامن بينهما يتم بشكل غير متزامن عبر الأحداث.

نصائح من خبرة أبو عمر

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

ليس لكل مشروع

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

تقبّل الاتساق النهائي (Eventual Consistency)

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

أهمية البنية التحتية

تحتاج إلى بنية تحتية قوية لدعم هذه المعمارية: متجر أحداث جيد (مثل EventStoreDB أو حتى PostgreSQL مع بعض التعديلات)، وناقل رسائل موثوق (مثل RabbitMQ أو Kafka) للتعامل مع نشر الأحداث ومعالجتها.

الخلاصة: من وحش إلى رفيق درب 😉

رحلتنا في ترويض وحش البيانات كانت صعبة لكنها مجدية. الانتقال إلى CQRS و Event Sourcing لم يحل مشاكل الأداء والتعقيد فحسب، بل فتح لنا آفاقًا جديدة من المرونة والقدرة على فهم نظامنا بشكل أعمق من أي وقت مضى. أصبح لدينا نظام لا يخاف من المستقبل، نظام يمكنه النمو والتطور مع احتياجات البزنس المتغيرة.

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

ودمتم سالمين.

أبو عمر

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

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

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

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

آخر المدونات

برمجة وقواعد بيانات

تحديثات قاعدة البيانات بدون توقف: كيف أنقذنا نمط التوسيع والتعاقد (Expand/Contract) من جحيم التوقفات المجدولة؟

هل سئمت من إيقاف الخدمة مع كل تحديث لهيكلة قاعدة البيانات؟ أشارككم قصة حقيقية وكيف أنقذنا نمط التوسيع والتعاقد (Expand/Contract) من ليالي النشر الطويلة والمُجهدة،...

4 يونيو، 2026 قراءة المزيد
الشبكات والـ APIs

كانت إعادة المحاولة كارثة: كيف أنقذتنا مفاتيح عدم تكرار العمليات (Idempotency Keys) من جحيم الفواتير المزدوجة؟

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

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

من التوقف التام إلى النجاة: كيف أنقذتنا استراتيجية “الضوء المرشد” (Pilot Light) يوم انقطعت السحابة؟

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

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

كانت مهمتي البرمجية للاختبار مجرد كود: كيف أنقذني توثيق القرارات من جحيم الصمت بعد المقابلة؟

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

4 يونيو، 2026 قراءة المزيد
التكنلوجيا المالية Fintech

من الانتظار لأيام إلى الدفع في ثوانٍ: كيف أنقذتنا شبكات الدفع الفوري من جحيم التحويلات البنكية؟

أسرد لكم من واقع تجربتي كـ "أبو عمر"، كيف عانينا من بطء وتكلفة التحويلات البنكية الدولية، وكيف جاءت شبكات الدفع الفوري ومعيار ISO 20022 لتكون...

4 يونيو، 2026 قراءة المزيد
البنية التحتية وإدارة السيرفرات

كان كل خادم لدينا ‘ندفة ثلج’ فريدة: كيف أنقذنا ‘الكود كبنية تحتية’ (IaC) من جحيم الانجراف اليدوي؟

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

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

كانت تغطية الاختبارات 100% لكن الأخطاء تتسرب: كيف أنقذنا “الاختبار الطفري” من جحيم الثقة الزائفة؟

كنا نظن أن تغطية الاختبار بنسبة 100% هي درعنا الواقي، لكن الأخطاء كانت تتسلل إلى الإنتاج كاللصوص في ليل بهيم. اكتشف كيف أنقذنا "الاختبار الطفري"...

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