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

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

في ليلة إطلاق الحملة، قعدت مع فنجان القهوة وأنا براقب لوحات المراقبة (Dashboards). أول ساعة كانت الأمور تمام، لكن فجأة، “ولعت الدنيا”. الطلبات بتزيد بالآلاف، والمستخدمين بضيفوا منتجات للسلة، وفي نفس الوقت، فريق التسويق والعمليات بحاولوا يسحبوا تقارير مبيعات مباشرة عشان يعرفوا أي منتجات “طايرة”.

وفجأة… النظام بدأ يتباطأ بشكل مرعب. صفحات المنتجات ما بتفتح، عمليات الدفع بتفشل، والتقارير بتعطي time out. قاعدة البيانات كانت بتصرخ وبتقول “ارحموني!”. كان شعور بالعجز، كإنك بنيت سفينة قوية، لكنك حطيت فيها محرك دراجة هوائية. يومها، زي ما بحكوها عنا، “جبنا العيد” بالعميل. بعد ليلة طويلة من الحلول المؤقتة وإعادة تشغيل الخوادم، عرفنا إنه لازم نلاقي حل جذري. المشكلة ما كانت في قوة الخوادم، بل في قلب تصميم النظام نفسه. من هون بدأت رحلتنا مع الـ CQRS.

ما هو أصل المشكلة؟ صراع القراءة والكتابة الأزلي

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

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

النموذج الواحد.. لعنة متنكرة في زي نعمة

المشكلة تتعمق أكثر لما ندرك أن متطلبات القراءة والكتابة مختلفة تماماً:

  • جانب الكتابة (Write Side): يهتم بالتحقق من صحة البيانات (Validation)، تطبيق منطق العمل (Business Logic) المعقد، والحفاظ على تناسق البيانات (Consistency). غالباً ما تكون نماذج الكتابة مصممة بشكل مترابط (Normalized) لضمان عدم تكرار البيانات.
  • جانب القراءة (Read Side): يهتم بالسرعة وعرض البيانات بالشكل اللي بتحتاجه واجهة المستخدم. غالباً ما نحتاج بيانات مجمّعة ومُعدّة مسبقاً (Denormalized) لتجنب عمليات الربط (JOINs) المعقدة والمكلفة في قاعدة البيانات.

عندما نحاول إرضاء الطرفين بنفس النموذج، نقع في فخ التعقيد. النموذج يصبح “ملغّم” بالـ if-statements، ويصعب فهمه وصيانته، والأسوأ من ذلك، أداؤه يتدهور لأن قاعدة البيانات تُجبر على القيام بعمليات معقدة لخدمة طلبات بسيطة.

الحل السحري: نمط CQRS (فصل السلطات يا جماعة!)

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

  1. مسار الأوامر (Commands): مسؤول عن أي عملية تهدف إلى تغيير حالة النظام (إنشاء، تحديث، حذف). الأمر هو نية للتغيير، مثل CreateOrderCommand. هذا المسار لا يُرجع بيانات.
  2. مسار الاستعلامات (Queries): مسؤول عن قراءة البيانات من النظام. الاستعلام لا يغير أي شيء في حالة النظام، ومهمته فقط جلب البيانات وعرضها.

الفكرة تشبه وجود دفترين في مكتبنا المزدحم من المثال السابق: دفتر للكتابة والتعديلات (يستخدمه شخص واحد في كل مرة بعناية)، ونسخ مُعدة مسبقاً من المعلومات المهمة (مثل دليل هاتف) للقراءة السريعة (يمكن للجميع استخدامه في نفس الوقت دون إزعاج الآخرين).

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

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

أولاً: الفصل المنطقي داخل الكود

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

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

لنفترض أننا نستخدم لغة C# مع ASP.NET Core. بدلاً من وجود ProductService ضخم، سنقوم بتقسيمه:

مثال: مسار الأوامر (Commands)

الأمر هو كائن بسيط يحمل البيانات اللازمة للتغيير. معالج الأمر (Handler) يحتوي على منطق العمل.


// Command: The intention to change something
public class UpdateProductPriceCommand
{
    public int ProductId { get; set; }
    public decimal NewPrice { get; set; }
}

// Command Handler: The logic to process the command
public class UpdateProductPriceCommandHandler
{
    private readonly IProductRepository _repository;

    public UpdateProductPriceCommandHandler(IProductRepository repository)
    {
        _repository = repository;
    }

    public void Handle(UpdateProductPriceCommand command)
    {
        // 1. Fetch the domain entity
        var product = _repository.GetById(command.ProductId);

        // 2. Execute business logic
        if (command.NewPrice <= 0)
        {
            throw new InvalidPriceException("Price must be positive.");
        }
        product.ChangePrice(command.NewPrice);

        // 3. Persist the change
        _repository.Save(product);
    }
}

مثال: مسار الاستعلامات (Queries)

الاستعلام ومعالجه يركزان فقط على جلب البيانات بكفاءة، وغالباً ما يُرجعان كائن نقل بيانات (DTO) بسيط ومسطح.


// Query: The request for data
public class GetProductDetailsQuery
{
    public int ProductId { get; set; }
}

// A simple DTO, optimized for the UI
public class ProductDetailsDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string FormattedPrice { get; set; }
    public int StockCount { get; set; }
}

// Query Handler: The logic to fetch and shape the data
public class GetProductDetailsQueryHandler
{
    private readonly AppDbContext _dbContext; // Using DbContext directly for performance

    public GetProductDetailsQueryHandler(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public ProductDetailsDto Handle(GetProductDetailsQuery query)
    {
        // Query directly to a DTO, bypassing the domain model completely
        return _dbContext.Products
            .Where(p => p.Id == query.ProductId)
            .Select(p => new ProductDetailsDto
            {
                Id = p.Id,
                Name = p.Name,
                FormattedPrice = p.Price.ToString("C"),
                StockCount = p.Stock.Quantity
            })
            .FirstOrDefault();
    }
}

لاحظ كيف أن معالج الاستعلام يتجاوز نموذج العمل (Domain Model) المعقد ويذهب مباشرة إلى قاعدة البيانات ليجلب البيانات التي يحتاجها فقط وبالشكل الذي يحتاجه. هذا يوفر أداءً هائلاً.

ثانياً: الفصل المادي (قواعد بيانات منفصلة)

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

  • قاعدة بيانات الكتابة (Write DB): يمكن أن تكون قاعدة بيانات علائقية (مثل PostgreSQL أو SQL Server) مصممة بشكل جيد (Normalized) لضمان دقة البيانات وتناسقها. هذه هي “مصدر الحقيقة” (Source of Truth).
  • قاعدة بيانات القراءة (Read DB): يمكن أن تكون أي شيء! قاعدة بيانات NoSQL مثل Elasticsearch للبحث السريع، أو Redis لتخزين البيانات المؤقتة (Caching)، أو حتى مجرد جداول مُسطّحة (Denormalized) في نفس قاعدة البيانات العلائقية. يتم تحسينها بالكامل لسرعة القراءة.

السؤال الآن: كيف نحافظ على تزامن البيانات بين قاعدة بيانات الكتابة وقاعدة بيانات القراءة؟

الجواب يكمن في استخدام نمط يعتمد على الأحداث (Event-Driven). عندما يتم تنفيذ أمر بنجاح في جانب الكتابة، فإنه يُصدر “حدثًا” (Event) مثل ProductPriceUpdated. هناك مكونات أخرى في النظام (Event Handlers) تستمع لهذه الأحداث وتقوم بتحديث قاعدة بيانات القراءة وفقاً لذلك. هذا يقودنا إلى مفهوم “الاتساق النهائي” (Eventual Consistency)، وهو موضوع كبير آخر، ولكنه الرفيق الطبيعي لنمط CQRS.

نصائح من قلب الميدان (من أبو عمر)

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

متى تستخدم CQRS؟ (مش لكل طبخة يا خبير!)

CQRS ليس حلاً لكل المشاكل. هو أداة قوية، لكن استخدامها في المكان الخطأ يسبب تعقيداً بلا فائدة. استخدمه في الحالات التالية:

  • الأنظمة التعاونية: عندما يعمل العديد من المستخدمين على نفس البيانات في نفس الوقت.
  • اختناقات الأداء: عندما تكون لديك متطلبات قراءة وكتابة مختلفة جداً وتسبب تضارباً.
  • منطق العمل المعقد: عندما يكون جانب الكتابة مليئاً بالقواعد والشروط المعقدة التي لا تريد أن تؤثر على سرعة القراءة.

متى تتجنب CQRS؟

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

الخلاصة: هل يستحق CQRS كل هذا العناء؟

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

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

أبو عمر

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

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

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

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

آخر المدونات

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

تحديثات قاعدة البيانات بدون توقف: كيف أنقذنا نمط التوسيع والتعاقد (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 قراءة المزيد
البودكاست