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

يا أهلاً وسهلاً فيكم يا جماعة، معكم أبو عمر.

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

كنا في غرفة عمليات حقيقية، القهوة شغالة 24 ساعة والتوتر سيد الموقف. بعد ساعات من التحليل والـ “Debugging” المؤلم، لقطت المشكلة الأساسية. المشكلة ما كانت في الكود بحد ذاته، لكن في “فلسفة” تصميم النظام كله. نموذج البيانات تبعنا كان في حالة حرب أهلية! نفس جدول المنتجات (Products) ونفس الـ Object Model كان عليه طلبات قراءة معقدة جداً عشان يعرض المنتجات مع فلاتر وأسعار وعروض، وبنفس الوقت عليه طلبات كتابة (تحديث) سريعة جداً مع كل عملية شراء بتصير عشان نخصم من المخزون.

عمليات القراءة كانت بتعمل “Lock” على الجداول عشان تضمن دقة البيانات، وعمليات الكتابة بتستنى، والعكس صحيح. صار الوضع أشبه بشارع إتجاه واحد والكل بحاول يمشي فيه بالإتجاهين بنفس الوقت. هنا، تذكرت نمط معماري كنت قرأت عنه زمان وحسيت إنه إجا وقته: الـ CQRS. كان هو طوق النجاة اللي طلعنا من هالجحيم.

المشكلة التقليدية: النموذج الواحد الذي يحكم الجميع (The One Model to Rule Them All)

قبل ما نغوص في حل الـ CQRS، خلينا نفهم أصل المشكلة اللي أغلبنا بوقع فيها. في معظم التطبيقات التقليدية، خصوصاً اللي بتبدأ بسيطة، بنستخدم نمط CRUD (Create, Read, Update, Delete) مع نموذج بيانات واحد. يعني، بيكون عنا `Product` object أو `User` entity، وهذا الكائن المسكين مسؤول عن كل شي:

  • بيحتوي على كل البيانات اللازمة لعرض تفاصيل المنتج في المتجر.
  • بيحتوي على منطق التحقق (Validation) قبل إنشاء أو تحديث المنتج.
  • يستخدم في عمليات معقدة مثل إنشاء طلب شراء جديد (تحديث المخزون).
  • يستخدم في توليد التقارير الإدارية.

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

  1. تعقيد النموذج (Model Bloat): الكائن بصير ضخم ومعقد، مليان خصائص ودوال مش كلها مطلوبة في كل سياق.
  2. تضارب الأداء (Performance Contention): عمليات القراءة المعقدة (Queries) تحتاج لجداول مترابطة (JOINs) وفهارس (Indexes) معينة، بينما عمليات الكتابة (Commands) تفضل نموذجاً بسيطاً ومباشراً لضمان السرعة والاتساق (Consistency). هذا الصراع هو سبب “الحرب الأهلية” في قاعدة البيانات.
  3. صعوبة التوسع (Scalability Issues): إذا كان عندك ضغط هائل على عمليات القراءة، بتضطر تعمل Scale-out للتطبيق كله، بما في ذلك جزء الكتابة اللي ممكن ما يكون عليه ضغط أصلاً. إنت بتكبر كل شي، مع إن المشكلة في جزء واحد بس.

الحل مع CQRS: فصل السلطات لإنهاء الحرب الأهلية

نمط CQRS، أو Command Query Responsibility Segregation، هو مبدأ بسيط لكن تأثيره عميق جداً. الفكرة الأساسية هي: “لا تستخدم نفس النموذج العقلي (والبرمجي) لتغيير البيانات وقراءتها”.

بدل ما يكون عنا نموذج واحد، الـ CQRS بقترح علينا نقسم نظامنا لجانبين واضحين:

  • جانب الأوامر (Command Side): مسؤول عن أي عملية بتغير حالة النظام (Create, Update, Delete). الأوامر هي نوايا، مثل “أنشئ منتجاً جديداً” أو “أضف هذا المنتج للسلة”.
  • جانب الاستعلامات (Query Side): مسؤول عن أي عملية بتقرأ بيانات من النظام لعرضها. الاستعلامات لا تغير أي شيء أبداً.

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

جانب الأوامر (The Write/Command Side)

هذا الجانب هو الحارس الأمين على بياناتك. كل تركيزه منصب على تنفيذ الأوامر بشكل صحيح وآمن.

  • النموذج: هنا نستخدم “Rich Domain Model” حقيقي. كائنات تحتوي على منطق العمل (Business Logic) وقواعد التحقق. مثلاً، كائن `Order` عنده دالة اسمها `AddItem()` وهي اللي بتتأكد إذا المنتج متوفر قبل إضافته.
  • الهدف: الاتساق (Consistency) وصحة البيانات فوق كل شيء.
  • التنفيذ: الأوامر لا تعيد بيانات. هي إما تنجح أو تفشل (بإرجاع خطأ). فكر فيها كأنك بتعطي أمر: “نفّذ وانتهى”.

مثال بسيط بالأكواد (C#) لكيف ممكن يبدو الأمر والـ Handler تبعه:


// الأمر: مجرد كائن بسيط يحمل البيانات اللازمة للتنفيذ
public class PlaceOrderCommand
{
    public Guid CustomerId { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

// معالج الأمر: هنا يكمن كل منطق العمل
public class PlaceOrderCommandHandler
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductRepository _productRepository;

    public PlaceOrderCommandHandler(IOrderRepository orderRepo, IProductRepository productRepo)
    {
        _orderRepository = orderRepo;
        _productRepository = productRepo;
    }

    public async Task Handle(PlaceOrderCommand command)
    {
        // 1. استرجاع الكائنات الغنية من قاعدة البيانات
        var customer = await _customerRepository.GetByIdAsync(command.CustomerId);
        var order = new Order(customer.Id);

        // 2. تنفيذ منطق العمل المعقد
        foreach (var item in command.Items)
        {
            var product = await _productRepository.GetByIdAsync(item.ProductId);
            order.AddItem(product, item.Quantity); // الدالة هنا تتأكد من المخزون وغيره
        }

        // 3. حفظ الحالة الجديدة
        await _orderRepository.SaveAsync(order);
    }
}

لاحظوا كيف أن الـ `Handle` method من نوع `Task` ولا يعيد أي بيانات. هذا هو جوهر جانب الأوامر.

جانب الاستعلامات (The Read/Query Side)

هذا الجانب هو الواجهة السريعة والجميلة لنظامك. كل همه هو عرض البيانات للمستخدم بأسرع وأنسب شكل ممكن.

  • النموذج: هنا لا نستخدم نماذج غنية أبداً. نستخدم ما يسمى بـ “Read Models” أو DTOs (Data Transfer Objects). هذه نماذج “غبية” ومسطحة، مصممة خصيصاً لكل شاشة في تطبيقك.
  • الهدف: السرعة والأداء. لا نهتم بالـ Business Logic هنا أبداً.
  • التنفيذ: الاستعلامات يمكن أن تستخدم قاعدة بيانات مختلفة تماماً! ممكن تكون نسخة للقراءة فقط (Read Replica)، أو قاعدة بيانات NoSQL مثل Elasticsearch للبحث السريع، أو حتى مجرد جداول معدة مسبقاً (Denormalized) ومحسّنة لعمليات القراءة.

مثال لكيف ممكن يبدو الاستعلام والـ Handler تبعه:


// الاستعلام: يحدد ما هي البيانات المطلوبة
public class GetOrderHistoryQuery
{
    public Guid CustomerId { get; set; }
}

// نموذج العرض: كائن مسطح مصمم خصيصاً لهذه الشاشة
public class OrderHistoryViewModel
{
    public Guid OrderId { get; set; }
    public DateTime OrderDate { get; set; }
    public string Status { get; set; }
    public decimal TotalPrice { get; set; }
}

// معالج الاستعلام: مهمته سهلة، فقط جلب البيانات
public class GetOrderHistoryQueryHandler
{
    private readonly IReadDatabaseConnection _dbConnection;

    public GetOrderHistoryQueryHandler(IReadDatabaseConnection dbConnection)
    {
        _dbConnection = dbConnection;
    }

    public async Task<List<OrderHistoryViewModel>> Handle(GetOrderHistoryQuery query)
    {
        // استعلام مباشر وسريع على قاعدة بيانات القراءة
        // ممكن يكون SQL خام أو استخدام Dapper مثلاً
        var sql = "SELECT OrderId, OrderDate, Status, TotalPrice FROM CustomerOrderSummaries WHERE CustomerId = @CustomerId";
        return await _dbConnection.QueryAsync<OrderHistoryViewModel>(sql, new { query.CustomerId });
    }
}

لاحظوا الفرق الشاسع! جانب الاستعلامات بسيط، مباشر، ومُحسَّن بالكامل لسرعة القراءة.

كيف نحافظ على تزامن البيانات بين الجانبين؟

هذا هو السؤال الذهبي. إذا كان لدينا قاعدة بيانات للكتابة وأخرى للقراءة، كيف نضمن أن البيانات التي نقرأها محدثة؟

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

لتطبيق هذا التزامن، هناك عدة طرق:

  1. الكتابة المزدوجة (Dual Writes): في الـ Command Handler، بعد الحفظ في قاعدة بيانات الكتابة، تقوم بتحديث قاعدة بيانات القراءة. هذه الطريقة بسيطة لكنها خطرة، فماذا لو فشلت الكتابة الثانية؟
  2. قوائم الرسائل (Messaging Queues): هذه هي الطريقة الأكثر احترافية وقوة. بعد أن ينجح الـ Command Handler في حفظ التغيير، يقوم بنشر “حدث” (Event) مثل `OrderPlaced` في Message Queue (مثل RabbitMQ أو Azure Service Bus). يكون هناك “مستمع” (Listener) منفصل يأخذ هذا الحدث ويقوم بتحديث قاعدة بيانات القراءة بناءً عليه. هذا يضمن أن التحديث سيتم بشكل موثوق.
  3. مزامنة على مستوى قاعدة البيانات: استخدام أدوات مثل Debezium لمراقبة التغييرات في قاعدة بيانات الكتابة (Change Data Capture) ودفعها إلى قاعدة بيانات القراءة.

نصائح أبو عمر الذهبية لتطبيق CQRS 💡

بعد ما خضنا هالتجربة، تعلمت كم درس مهم حابب أشارككم فيها:

  • لا تطبق CQRS على كل شيء

    الـ CQRS يضيف طبقة من التعقيد. لا تستخدمه في الأجزاء البسيطة من نظامك. ابدأ بتطبيقه فقط على الأجزاء المعقدة والتي تعاني من تضارب الأداء (مثل إدارة المنتجات والمخزون والطلبات في مثالنا). باقي الأجزاء (مثل إدارة المستخدمين البسيطة) يمكن أن تبقى على نمط CRUD التقليدي.

  • تقبّل “الاتساق النهائي” وتحدث مع فريق العمل

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

  • يمكنك البدء بنفس قاعدة البيانات

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

  • فكر في Event Sourcing كصديق مقرب لـ CQRS

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

الخلاصة: من الفوضى إلى النظام

في النهاية، نمط CQRS كان أكثر من مجرد حل تقني لمشكلة أداء. لقد كان تغييراً في طريقة تفكيرنا كفريق. أجبرنا على تحديد المسؤوليات بوضوح، وفصل ما هو مهم لعملية الكتابة (الدقة والمنطق) عما هو مهم لعملية القراءة (السرعة وسهولة العرض). لقد أنهى الحرب الأهلية في بياناتنا وأحلّ السلام والنظام.

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

أتمنى تكونوا استفدتوا، وإلى لقاء في مقال جديد.

أبو عمر

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

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

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

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

آخر المدونات

أدوات وانتاجية

كانت بيئة التطوير لدينا كلاً في وادٍ: كيف أنقذتنا ‘حاويات التطوير’ (Dev Containers) من جحيم ‘إنها تعمل على جهازي’؟

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

28 أبريل، 2026 قراءة المزيد
نصائح برمجية

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

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

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

كانت قوائمنا تربك المستخدمين: كيف أنقذنا ‘قانون هيك’ (Hick’s Law) من جحيم شلل الاختيار؟

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

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

كانت تقاريرنا تتجمد: كيف أنقذتنا ‘المشاهد المادية’ (Materialized Views) من جحيم الاستعلامات التحليلية؟

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

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

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

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

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