عمليات القراءة والكتابة في حرب أهلية: كيف أنقذ نمط CQRS نظامنا من جحيم التعقيد؟

حرب أهلية في قلب قاعدة البيانات

يا جماعة الخير، اسمحوا لي أرجع بالذاكرة كم سنة لورا. كنا شغالين على نظام إدارة موارد ضخم لشركة كبيرة، نظام فيه كل شي ممكن تتخيلوه: إدارة مخزون، فواتير، حسابات عملاء، تقارير تحليلية… يعني “من كل قطر أغنية”. في البداية، الأمور كانت “عال العال”. بنينا النظام بالطريقة التقليدية اللي تعلمناها كلنا: نموذج بيانات واحد (One Model to Rule Them All)، وخدمات (Services) بتتعامل مع هاد النموذج لكل شي: إضافة منتج جديد، تحديث سعره، عرض قائمة المنتجات في لوحة التحكم، استخراج تقرير مبيعات ربع سنوي.

لكن مع الوقت، ومع زيادة البيانات والمستخدمين، بلشت المشاكل تظهر. صارت عمليات القراءة (Queries) بطيئة جداً. تقرير المبيعات اللي كان ياخد ثواني، صار ياخد دقايق وممكن يعمل timeout. ليش؟ لأنه عشان يعرضلك ملخص بسيط، كان مضطر يعمل joins معقدة بين عشرات الجداول. وفي نفس الوقت، عمليات الكتابة (Commands) زي إضافة طلب جديد أو تحديث المخزون، كانت بتتأثر وبتصير بطيئة، وأحياناً تفشل بسبب الـ database locks اللي بتعملها التقارير الطويلة.

كنا حرفياً في حالة حرب أهلية داخل قاعدة البيانات. فريق “القراءة” بده بيانات مُجمّعة ومُبسّطة للعرض السريع، وفريق “الكتابة” بده بيانات مُفصّلة ودقيقة مع كل القيود والقواعد (constraints and business rules) عشان يضمن سلامة البيانات. والضحية؟ كان أداء النظام وتجربة المستخدم.

في اجتماع من الاجتماعات اللي كلها توتر، واحد من الشباب صاح: “يا عمي هدول الاثنين ما بنفع يعيشوا ببيت واحد!”. وقتها لمعت براسي فكرة نمط كنت قرأت عنه زمان وحسيته معقد زيادة عن اللزوم: CQRS. قلت لحالي، يمكن هلأ وقته. يمكن هاد هو الحل اللي بيفصل بين المتخاصمين وبيعمل صلح.

المشكلة الأساسية: نموذج “الكل في واحد”

قبل ما نحكي عن الحل، خلينا نفهم أصل المشكلة. معظمنا بيبدأ تطبيقاته بنمط CRUD (Create, Read, Update, Delete) الكلاسيكي. بيكون عنا نموذج (Model) واحد بيمثل كيان معين، مثلاً `Product`.

هذا النموذج بيكون غني ومليان تفاصيل:

  • معلومات أساسية (ID, Name, SKU)
  • معلومات تسويقية (Description, Images, SEO tags)
  • معلومات تسعير (Price, Discount)
  • معلومات مخزون (StockQuantity, WarehouseLocation)
  • علاقات مع كيانات أخرى (Categories, Reviews, Supplier)

المشكلة إنه هاد النموذج الواحد بنستخدمه لكل شي:

  1. للكتابة (Write): لما بدنا ننشئ منتج جديد، لازم نعبّي كل الحقول الإجبارية ونتأكد من كل قواعد العمل (Business Logic).
  2. للقراءة (Read): لما بدنا نعرض قائمة منتجات في صفحة المتجر الرئيسية، إحنا بس بحاجة للاسم والصورة والسعر. لكننا بنحمّل النموذج الكامل من قاعدة البيانات (Over-fetching). ولما بدنا نعرض صفحة تفاصيل المنتج، بنحتاج معظم البيانات. ولما بدنا نعمل تقرير عن المنتجات الأكثر مبيعاً، بنحتاج نعمل حسابات معقدة على هاد النموذج.

هذا الخلط بين متطلبات القراءة والكتابة على نفس النموذج هو اللي بيخلق الجحيم اللي كنا فيه. عمليات القراءة بتصير معقدة وبطيئة لأن النموذج مصمم ليكون دقيقاً ومتوافقاً مع قواعد الكتابة (Normalized for writes). وعمليات الكتابة ممكن تتعطل بسبب الضغط اللي بتسببه عمليات القراءة.

الحل السحري: نمط فصل مسؤوليات الأوامر والاستعلامات (CQRS)

CQRS هو اختصار لـ Command Query Responsibility Segregation. الاسم طويل ومعقد، بس الفكرة بسيطة بشكل عبقري: “أي عملية بتغيّر حالة النظام هي ‘أمر’ (Command)، وأي عملية بتسترجع بيانات بدون ما تغيّرها هي ‘استعلام’ (Query). وهدول الشغلتين لازم يكونوا منفصلين تماماً”.

بدل ما يكون عنا نموذج واحد، بصير عنا نموذجين (أو أكثر):

  • نموذج الكتابة (Write Model): هذا هو النموذج الغني والمعقد اللي فيه كل قواعد العمل والـ validation. مهمته الوحيدة هي تنفيذ الأوامر وتغيير حالة النظام. ما برجع بيانات، بالكثير برجع تأكيد (Success/Fail) أو ID للكيان الجديد.
  • نموذج القراءة (Read Model): هذا نموذج بسيط، “غبي” ومسطح (flat). هو عبارة عن DTOs (Data Transfer Objects) مصممة خصيصاً لكل شاشة أو واجهة عرض. مهمته الوحيدة هي توفير البيانات للاستعلامات بأسرع شكل ممكن.

كيف يبدو هذا على أرض الواقع؟

خلونا ناخد مثال عملي. تخيل عنا أمر لتغيير سعر منتج. بالطريقة القديمة، كان ممكن يكون عنا هيك شي (مثال بـ C#):


// الطريقة القديمة: خدمة واحدة لكل شيء
public class ProductService
{
    private readonly DbContext _context;

    public Product UpdateProductPrice(int productId, decimal newPrice)
    {
        var product = _context.Products.Find(productId);
        if (product == null) throw new Exception("Product not found");

        // ... منطق معقد للتحقق من صلاحيات وقواعد العمل ...
        product.Price = newPrice;
        
        _context.SaveChanges();
        return product; // نرجع النموذج الكامل مع كل بياناته
    }

    public ProductViewModel GetProductForDisplay(int productId)
    {
        // ... تحميل النموذج الكامل ثم تحويله لنموذج عرض ...
    }
}

مع CQRS، بنفصل هاي المسؤوليات:

1. جانب الأوامر (The Command Side)

الأمر هو مجرد كائن بسيط بحمل النية (Intent) والبيانات اللازمة.


// الأمر: كائن بسيط يحمل البيانات
public class UpdateProductPriceCommand
{
    public int ProductId { get; set; }
    public decimal NewPrice { get; set; }
}

// معالج الأمر: هو اللي فيه الشغل الحقيقي
public class UpdateProductPriceCommandHandler
{
    private readonly WriteDbContext _context; // لاحظ استخدام سياق بيانات خاص بالكتابة

    public void Handle(UpdateProductPriceCommand command)
    {
        // 1. تحميل النموذج الكامل (Domain Model)
        var product = _context.Products.Find(command.ProductId);
        if (product == null) throw new Exception("Product not found");

        // 2. تطبيق قواعد العمل المعقدة
        product.ChangePrice(command.NewPrice); // قد يحتوي هذا الـ method على business logic

        // 3. حفظ التغييرات
        _context.SaveChanges();
        // لا نرجع أي بيانات! المهمة انتهت.
    }
}

2. جانب الاستعلامات (The Query Side)

الاستعلام هو طلب للبيانات، ونموذج القراءة مصمم خصيصاً له.


// نموذج القراءة: بسيط ومسطح ومخصص لواجهة العرض
public class ProductDetailsViewModel
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Description { get; set; }
    public double AverageRating { get; set; }
}

// معالج الاستعلام: مهمته جلب البيانات بأسرع طريقة
public class GetProductDetailsQueryHandler
{
    private readonly ReadDbContext _context; // سياق بيانات خاص بالقراءة

    public ProductDetailsViewModel Handle(int productId)
    {
        // استعلام مباشر ومُحسّن للحصول على البيانات المطلوبة فقط
        // ممكن يكون هذا الاستعلام على View في قاعدة البيانات أو جدول Denormalized
        var productView = _context.ProductDetailsViews
                                  .FirstOrDefault(p => p.ProductId == productId);
        
        return productView;
    }
}

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

القوة الحقيقية: عندما تنفصل قواعد البيانات

في البداية، يمكنك تطبيق CQRS باستخدام نفس قاعدة البيانات. مجرد فصل المنطق في الكود بحد ذاته بيعطي فوائد تنظيمية كبيرة. لكن القوة الحقيقية لـ CQRS بتظهر لما تفصل قواعد البيانات نفسها:

  • قاعدة بيانات الكتابة (Write DB): بتكون قاعدة بيانات علائقية (Relational) مثل PostgreSQL أو SQL Server. تصميمها بيكون Normalized (النموذج الثالث الطبيعي 3NF) لضمان سلامة البيانات وتجنب التكرار. هي مُحسّنة لعمليات الـ Transactions السريعة (OLTP).
  • قاعدة بيانات القراءة (Read DB): ممكن تكون أي شي! ممكن تكون نسخة Denormalized من البيانات في قاعدة بيانات علائقية (Materialized Views)، أو قاعدة بيانات NoSQL مثل Elasticsearch للبحث السريع، أو MongoDB للبيانات المرنة، أو حتى Redis لتخزين البيانات المؤقتة (Caching). هي مُحسّنة لعمليات القراءة الكثيفة والسريعة (OLAP).

“بس يا أبو عمر، كيف بتضل قاعدة بيانات القراءة محدّثة؟”

سؤال ممتاز. هنا يأتي دور المزامنة. بعد أن يقوم جانب الكتابة (Command) بتحديث قاعدة بياناته، يقوم بنشر “حدث” (Event) يقول “لقد تم تحديث سعر المنتج X”. هذا الحدث يتم إرساله عبر ناقل رسائل (Message Bus) مثل RabbitMQ أو Kafka. هناك “مستمع” (Listener) يستقبل هذا الحدث ويقوم بتحديث قاعدة بيانات القراءة (Read DB) بالمعلومات الجديدة.

هذا يقودنا لمفهوم “الاتساق النهائي” (Eventual Consistency). يعني أن قاعدة بيانات القراءة قد لا تكون محدّثة 100% في نفس اللحظة، قد يكون هناك تأخير بسيط (أجزاء من الثانية). وهذا مقبول في 99% من الحالات. عرض سعر منتج قديم لثانية واحدة على واجهة المتجر ليس كارثة.

نصائح من خبرة “الختيار” 🧔

بعد ما طبقنا هاد النمط وحلينا الحرب الأهلية في نظامنا، تعلمت كم شغلة بحب أشارككم فيها:

1. ليس لكل داء دواء

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

2. ابدأ بسيطاً

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

3. فكّر بـ “الاتساق النهائي”

إذا قررت استخدام قواعد بيانات منفصلة، يجب أن تتقبل أنت وفريقك وفهم العميل لفكرة “Eventual Consistency”. يجب تحديد أجزاء النظام التي تتطلب اتساقاً فورياً (Transactional Consistency) – مثل عمليات الدفع – والأجزاء التي يمكن أن تتحمل تأخيراً بسيطاً.

4. استعن بالأدوات

لا تعيد اختراع العجلة. هناك مكتبات ممتازة تساعد في تطبيق CQRS بسهولة، مثل مكتبة MediatR في عالم الـ .NET. هذه المكتبات توفر لك آلية بسيطة لإرسال الأوامر والاستعلامات إلى معالجاتها المناسبة بدون الحاجة لكتابة كود توصيل (boilerplate) معقد.

الخلاصة: السلام خير من ألف نموذج معقد

في النهاية، كان نمط CQRS هو “الدبلوماسي” الذي أنهى الحرب الأهلية في نظامنا. أعطى لكل طرف ما يحتاجه: جانب الكتابة حصل على نموذجه الدقيق والآمن، وجانب القراءة حصل على نماذجه السريعة والمُحسّنة للعرض. النتيجة كانت نظاماً أسرع، أكثر استقراراً، وأسهل بكثير في الصيانة والتطوير.

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

أبو عمر

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

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

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

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

آخر المدونات

نصائح برمجية

كودنا كان مليئاً بالأرقام الغامضة: كيف أنقذتنا ‘التعدادات’ (Enums) من جحيم الأرقام السحرية؟

أتذكر ليلة طويلة من تصحيح الأخطاء، كان السبب رقماً غامضاً في الكود. في هذه المقالة، أشارككم قصة كيف أنقذتنا التعدادات (Enums) من فوضى "الأرقام السحرية"،...

24 أبريل، 2026 قراءة المزيد
ذكاء اصطناعي

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

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

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

كنا نكتب كل صفحة هبوط يدوياً: كيف أنقذنا ‘التحسين البرمجي لمحركات البحث’ (Programmatic SEO) من جحيم المحتوى المحدود؟

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

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

موقعنا كان حصناً منيعاً أمام ذوي الإعاقة: كيف أنقذتنا ‘إرشادات الوصول إلى محتوى الويب’ (WCAG) من جحيم الاستبعاد الرقمي؟

أنا أبو عمر، مبرمج فلسطيني. كنت أظن أن موقعنا تحفة فنية، حتى جاءتني رسالة من صديق كفيف قائلاً: "موقعك جميل، لكنني لا أرى منه شيئاً"....

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

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

وداعاً ليالي الصيانة الطويلة والمستخدمين الغاضبين! في هذه المقالة، أشارككم قصة حقيقية وكيف غيرت استراتيجيات 'الهجرات بدون توقف' (Zero-Downtime Migrations) طريقة عملنا، مع دليل عملي...

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

فاتورة السحابة كانت لغزاً: كيف أنقذتنا ‘عمليات الإدارة المالية’ (FinOps) من جحيم الإنفاق غير المتوقع؟

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

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