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

ليلة لا تُنسى: حين أدركت أن تطبيقي يحتضر

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

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

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

ما هو نمط CQRS؟ شرح بسيط لغير المختصين

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

تخيل أن تطبيقك هو مطعم كبير. في هذا المطعم، لديك قسمان رئيسيان:

  1. المطبخ (The “Write” Side – الأوامر): هنا يتم استقبال الطلبات من الزبائن (أوامر مثل “اطبخ لي طبق كذا”). الطهاة يغيرون حالة المكونات الخام ويحولونها إلى طبق جاهز. هذه عمليات “كتابة” أو “تعديل” (Commands). هدف المطبخ هو الدقة وتنفيذ الطلب بشكل صحيح حسب الوصفة (Business Logic).
  2. قائمة الطعام وقسم الاستعلامات (The “Read” Side – الاستعلامات): هنا يجلس الزبون ويقرأ قائمة الطعام (استعلام “ما هي الأطباق المتوفرة؟”)، أو يسأل النادل عن حالة طلبه (استعلام “هل طلبي جاهز؟”). هذه عمليات “قراءة” (Queries). هدف هذا القسم هو السرعة وتقديم المعلومة بشكل واضح ومباشر للزبون.

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

نمط CQRS يقول: “لا يا عمي، افصلهم!”. دعنا نبني نموذجين مختلفين تمامًا:

  • نموذج الأوامر (Command Model): مسؤول فقط عن تنفيذ الأوامر التي تغير حالة النظام (إنشاء، تحديث، حذف). هذا النموذج يركز على صحة البيانات ومنطق العمل المعقد. لا يُرجع بيانات، بل مجرد تأكيد على النجاح أو الفشل.
  • نموذج الاستعلامات (Query Model): مسؤول فقط عن قراءة البيانات وتقديمها للمستخدم. هذا النموذج يتم تحسينه (Optimized) ليكون فائق السرعة. يمكن أن يكون نسخة مبسطة أو مختلفة تمامًا من البيانات الأصلية، مصممة خصيصًا للعرض.

نصيحة من أبو عمر: أبسط طريقة لفهم الفرق هي أن تسأل نفسك: “هل هذا الكود سيغير أي بيانات في قاعدة البيانات؟”. إذا كان الجواب “نعم”، فهو Command. إذا كان “لا”، فهو Query.

متى تحتاج حقًا إلى CQRS؟ (ليس كل صداع يحتاج جراحة)

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

العلامات التي تشير إلى أن تطبيقك يصرخ طالبًا النجدة

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

متى يجب أن تبتعد عن CQRS؟

  • التطبيقات البسيطة (CRUD): إذا كان تطبيقك مجرد واجهات لإضافة وتعديل وحذف بيانات بسيطة دون منطق معقد، فإن CQRS سيكون تعقيدًا لا مبرر له.
  • فرق العمل الصغيرة أو غير الخبيرة: يتطلب النمط فهمًا جيدًا لمفاهيم متقدمة مثل “الاتساق النهائي” (Eventual Consistency)، وقد يكون مربكًا للفريق في البداية. ما تغلب حالك وتفوت في متاهة إذا تطبيقك بسيط.

رحلة التطبيق العملي: من الفكرة إلى الكود

دعنا نأخذ مثالاً بسيطًا: نظام تدوين. سنرى كيف يمكننا تطبيق CQRS لإنشاء مقال جديد (Command) وقراءة تفاصيل المقال (Query).

(الأمثلة التالية مكتوبة بلغة C#، لكن المبدأ يطبق على أي لغة برمجة)

1. نموذج الكتابة (The Write Model)

هنا، نركز على الأمر نفسه ومعالجه. الأمر هو مجرد حاوية بيانات (DTO) تحمل النية بالتغيير.


// الأمر: "أريد إنشاء مقال جديد بهذه التفاصيل"
public class CreatePostCommand
{
    public Guid PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public string Author { get; set; }
}

// معالج الأمر: الكود الفعلي الذي ينفذ الأمر
public class CreatePostCommandHandler
{
    // هذا يتصل بقاعدة البيانات الخاصة بالكتابة (Write DB)
    private readonly IWritePostRepository _repository; 

    public CreatePostCommandHandler(IWritePostRepository repository)
    {
        _repository = repository;
    }

    // هذه الدالة لا ترجع بيانات، فقط تنفذ المهمة
    public async Task Handle(CreatePostCommand command)
    {
        // 1. التحقق من صحة البيانات (Validation)
        if (string.IsNullOrWhiteSpace(command.Title))
        {
            throw new Exception("Title cannot be empty.");
        }

        // 2. تطبيق أي منطق عمل (Business Logic)
        // مثلاً: التحقق من عدم وجود مقال بنفس العنوان

        // 3. إنشاء كائن المجال (Domain Object) وحفظه
        var post = new Post(command.PostId, command.Title, command.Content, command.Author);
        await _repository.SaveAsync(post);
        
        // 4. (خطوة متقدمة) نشر حدث "PostCreatedEvent" لإعلام الأنظمة الأخرى
    }
}

لاحظ كيف أن هذا الجزء من الكود يركز 100% على مهمة واحدة: إنشاء المقال بشكل صحيح وآمن.

2. نموذج القراءة (The Read Model)

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


// الاستعلام: "أعطني تفاصيل المقال رقم كذا"
public class GetPostDetailsQuery
{
    public Guid PostId { get; set; }
}

// نموذج العرض (ViewModel): شكل البيانات النهائي الذي سيُعرض
public class PostDetailsViewModel
{
    public Guid Id { get; set; }
    public string PostTitle { get; set; } // قد يكون اسم الحقل مختلفًا
    public string HtmlContent { get; set; } // قد يكون المحتوى بتنسيق مختلف
    public string AuthorName { get; set; }
    public int CommentCount { get; set; } // بيانات مجمعة ومحسوبة مسبقًا
    public DateTime LastUpdate { get; set; }
}

// معالج الاستعلام: الكود الذي يجلب البيانات بسرعة
public class GetPostDetailsQueryHandler
{
    // هذا يتصل بقاعدة بيانات القراءة (Read DB) المحسّنة
    private readonly IReadDbContext _context;

    public GetPostDetailsQueryHandler(IReadDbContext context)
    {
        _context = context;
    }

    // هذه الدالة ترجع البيانات الجاهزة للعرض
    public async Task<PostDetailsViewModel> Handle(GetPostDetailsQuery query)
    {
        // استعلام مباشر وسريع على نموذج القراءة المبسّط (Denormalized)
        // لا يوجد أي منطق عمل هنا، فقط جلب بيانات
        var postView = await _context.PostDetailsViews
                                     .FirstOrDefaultAsync(p => p.Id == query.PostId);
        
        return postView;
    }
}

هل ترى الفرق؟ نموذج القراءة لا يهتم بكيفية إنشاء المقال أو قواعد العمل. كل همه هو أن يجلب لك `PostDetailsViewModel` بأسرع طريقة ممكنة. قد تكون قاعدة بيانات القراءة هذه جدولاً واحدًا فقط تم تجميع البيانات فيه مسبقًا، أو حتى نسخة مخبأة في ذاكرة Redis.

3. التحدي الأكبر: مزامنة البيانات بين النموذجين

هنا يكمن التحدي الرئيسي في CQRS. عندما يقوم `CreatePostCommandHandler` بحفظ مقال جديد في قاعدة بيانات الكتابة، كيف يصل هذا التغيير إلى قاعدة بيانات القراءة؟

الجواب هو: بشكل غير متزامن (Asynchronously). وهذا يقودنا إلى مفهوم الاتساق النهائي (Eventual Consistency)، ويعني أن نموذج القراءة قد لا يكون محدثًا بنسبة 100% في نفس اللحظة التي يتم فيها التغيير، ولكنه سيصبح محدثًا في النهاية (عادة في أجزاء من الثانية).

الطريقة الشائعة لتحقيق ذلك هي باستخدام “ناقل الأحداث” (Event Bus) مثل RabbitMQ أو Azure Service Bus.

  1. بعد أن يحفظ `CreatePostCommandHandler` المقال بنجاح، يقوم بنشر حدث اسمه `PostCreatedEvent` على ناقل الأحداث.
  2. يوجد “مستمع” (Listener) أو (Subscriber) آخر في مكان ما في النظام، وظيفته الوحيدة هي الاستماع لهذا الحدث.
  3. عندما يستقبل المستمع حدث `PostCreatedEvent`، فإنه يقرأ تفاصيل المقال الجديد ويقوم بتحديث/إنشاء السجل المقابل في قاعدة بيانات القراءة.

نصيحة عملية من أبو عمر: لازم تفهّم العميل وصاحب المنتج شو يعني “Eventual Consistency”. اشرح له أن لوحة التحكم قد تتأخر ثانية واحدة لتُظهر التغيير الجديد. هذا أفضل من أن يفتح لك تذاكر دعم فني كل شوي ويقول “أضفت منتجًا ولكنه لم يظهر فورًا!”. إدارة التوقعات نصف المعركة.

خلاصة الكلام والنصيحة الأخيرة من أبو عمر 💡

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

الخلاصة هي:

  • استخدم CQRS عندما يكون لديك نظام معقد به صراع واضح بين متطلبات القراءة والكتابة.
  • تجنب CQRS في التطبيقات البسيطة والمباشرة لأنه سيضيف تعقيدًا لا داعي له.
  • 🤔 ابدأ بسيطًا دائمًا. لا تبدأ مشروعك الجديد بنمط CQRS من اليوم الأول. ابدأ بالبنية التقليدية البسيطة. عندما تبدأ بالشعور بنفس الألم الذي وصفته في قصتي – بطء، تعقيد، تضارب – حينها فقط، ابدأ بالتفكير في التحول التدريجي نحو CQRS.

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

يلا شدوا حيلكم، وأي سؤال أنا حاضر. سلام!

أبو عمر

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

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

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

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

آخر المدونات

البنية التحتية وإدارة السيرفرات

تطبيقاتي كانت تموت وتعود للحياة: كيف أنقذتني ‘مسابير الحياة والجاهزية’ من جحيم CrashLoopBackOff في Kubernetes

أشارككم قصة حقيقية من تجربتي كمطور، عندما كانت تطبيقاتي تنهار بشكل متكرر في Kubernetes. سأشرح لكم بالتفصيل كيف أنقذتني مسابير الحياة والجاهزية (Liveness & Readiness...

5 أبريل، 2026 قراءة المزيد
ادارة الفرق والتنمية البشرية

جلسات التقييم كانت حرباً كلامية: كيف أنقذني نموذج ‘الموقف-السلوك-التأثير’ (SBI) من جحيم التغذية الراجعة الهدّامة؟

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

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

واجهاتي كانت تتكسر بصمت: كيف أنقذني ‘الاختبار البصري الانحداري’ من جحيم الأخطاء المرئية؟

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

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

سجلاتي كانت بلا فائدة عند الطوارئ: كيف أنقذني ‘التسجيل المنظم’ (Structured Logging) من جحيم التنقيح الأعمى؟

أشارككم قصة حقيقية حول كارثة إنتاجية كادت أن تشلّ نظامنا، وكيف كانت سجلاتنا النصية العادية عديمة الفائدة. اكتشفوا معي مفهوم "التسجيل المنظم" (Structured Logging) الذي...

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

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

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

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