كانت عمليات القراءة والكتابة في صراع دائم: كيف أنقذنا نمط 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 ليس مجرد تقنية، بل هو طريقة تفكير. يجبرك على التمييز بين “الأمر” و”الاستعلام”، بين “التغيير” و”المشاهدة”. هذا الفصل هو مفتاح بناء أنظمة قوية وقابلة للتطوير وقادرة على مواجهة التحديات الحقيقية في عالم البرمجيات الحديث. هو ليس سهلاً، ويتطلب فهماً أعمق، لكنه استثمار يؤتي ثماره أضعافاً مضاعفة في المشاريع الصحيحة. 💡

أبو عمر

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

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

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

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

آخر المدونات

التوسع والأداء العالي والأحمال

كان فشل خدمة واحدة يُسقط نظامنا بالكامل: كيف أنقذنا نمط ‘قاطع الدائرة’ (Circuit Breaker) من جحيم الانهيارات المتتالية؟

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

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

كانت قراراتنا الائتمانية صندوقاً أسود: كيف أنقذنا ‘الذكاء الاصطناعي القابل للتفسير’ (XAI) من جحيم التحيز والشكاوى التنظيمية؟

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

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

كانت أعطالنا تباغتنا في منتصف الليل: كيف أنقذنا Prometheus من جحيم المراقبة التفاعلية؟

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

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

طلبات الدمج تموت في الانتظار: كيف أنقذ “ميثاق مراجعة الكود” فريقنا من جحيم التأخير والجدل؟

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

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

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

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

16 مايو، 2026 قراءة المزيد
أتمتة العمليات

كان مطورنا الجديد ينتظر أياماً: كيف أنقذتنا ‘أتمتة إعداد البيئة’ من جحيم الأسبوع الأول الضائع؟

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

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

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

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

15 مايو، 2026 قراءة المزيد
​معمارية البرمجيات

كانت خدماتنا تتحدث في نفس الوقت: كيف أنقذتنا ‘المعمارية القائِمَة على الأحداث’ (EDA) من جحيم الاقتران المحكم؟

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

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