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

أبو عمر

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

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

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

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

آخر المدونات

ذكاء اصطناعي

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

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

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

كنا نحرق الذاكرة لحساب المستخدمين الفريدين: كيف أنقذتنا خوارزمية HyperLogLog من جحيم استهلاك الموارد؟

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

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

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

أتذكر جيداً أيام الملاحقة اليدوية للكلمات المفتاحية الطويلة، جهدٌ ضائع ووقتٌ مهدر. في هذه المقالة، أشارككم قصة كيف غيّر "التحسين البرمجي لمحركات البحث" (Programmatic SEO)...

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

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

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

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

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

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

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

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

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

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

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

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

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