قاعدة بياناتي لم تعد تحتمل: رحلتي لإنقاذ الأداء بفصل الأوامر عن الاستعلامات (CQRS)

يا أهلاً وسهلاً بكل المبرمجين والمبرمجات، معكم أخوكم أبو عمر.

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

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

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

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

وهون يا جماعة الخير، بعد ليلة طويلة من القهوة والتفكير، لمعت في بالي فكرة كنت قرأت عنها زمان: CQRS – Command Query Responsibility Segregation. كانت تبدو معقدة نظرياً، لكنها كانت بالضبط ما نحتاجه. ومن هنا، بدأت رحلتي الحقيقية لإنقاذ النظام.

ما هو نمط CQRS أصلاً؟ (الشرح المبسط)

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

في أي نظام، عندك نوعين رئيسيين من العمليات:

  1. الأوامر (Commands): هي أي عملية تُغير حالة النظام. فكر فيها كأفعال أمر: “أنشئ مستخدم”، “حدّث سعر المنتج”، “احذف الطلب”. هذه العمليات عادةً ما تكون معقدة، فيها منطق عمل (Business Logic) وقواعد تحقق (Validation)، والأهم أنها لا تُرجع بيانات (أو ترجع فقط حالة النجاح/الفشل).
  2. الاستعلامات (Queries): هي أي عملية تقرأ البيانات من النظام دون تغييرها. فكر فيها كأسئلة: “أعطني كل المنتجات”، “ابحث عن طلب العميل فلان”. هذه العمليات يجب أن تكون سريعة جداً وبسيطة، ومهمتها فقط جلب البيانات لعرضها.

نمط CQRS بقول بكل بساطة: “ليش نخلط الزيت بالمي؟”. خلينا نفصل مسار تنفيذ الأوامر عن مسار تنفيذ الاستعلامات. بدل ما يكون عنا موديل واحد وقاعدة بيانات واحدة بتحاول تخدم الجهتين، بنعمل مسارين مختلفين، كل واحد مُحسّن ومُصمم خصيصاً لمهمته.

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

لماذا نحتاج إلى هذا التعقيد؟ المشكلة في النهج التقليدي

النهج التقليدي اللي معظمنا تعلمناه هو CRUD (Create, Read, Update, Delete) باستخدام موديل واحد (مثل Active Record أو Entity Framework) لكل شيء. هذا النهج رائع للتطبيقات البسيطة، لكنه يبدأ بالانهيار مع نمو التطبيق وتعقيده، ولعدة أسباب:

1. نماذج بيانات متضاربة (Conflicting Data Models)

الموديل الذي تحتاجه لعملية “إنشاء طلب جديد” يختلف تماماً عن الموديل الذي تحتاجه لعرض “ملخص الطلبات في لوحة التحكم”.

  • موديل الكتابة (Write Model): يحتاج إلى خصائص كثيرة، قواعد تحقق معقدة، وعلاقات متشابكة لضمان سلامة البيانات.
  • موديل القراءة (Read Model): يحتاج إلى بيانات “مسطحة” (Flattened) وجاهزة للعرض، وغالباً ما تكون تجميعاً لمعلومات من عدة جداول.

في النهج التقليدي، نحاول حشر كل هذه المتطلبات في كلاس واحد، فينتج عنا موديل “سمين” (Fat Model) ومعقد وصعب الصيانة.

2. اختناقات الأداء (Performance Bottlenecks)

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

  • الكتابة تحتاج إلى أقفال (Locks) على السجلات والجداول لضمان تكامل البيانات (Consistency).
  • القراءة تتأثر بهذه الأقفال، فتبدأ بالتباطؤ والانتظار.

عندما يكون حجم القراءة أكبر بكثير من حجم الكتابة (وهو الحال في 90% من التطبيقات)، فإن عمليات الكتابة القليلة قد تتسبب في شل حركة النظام بأكمله.

3. صعوبة التحسين (Optimization Difficulty)

من الصعب جداً تحسين قاعدة بيانات واحدة لخدمة نوعين متناقضين من الأعباء:

  • لتحسين القراءة: أنت بحاجة إلى الكثير من الفهارس (Indexes)، وتكرار البيانات (Denormalization) لتقليل عمليات الربط (JOINs)، واستخدام طرق عرض (Views).
  • لتحسين الكتابة: أنت تريد تقليل عدد الفهارس (لأن كل فهرس يضيف عبئاً عند الكتابة)، وتوحيد البيانات (Normalization) لتجنب التكرار وضمان الاتساق.

محاولة إرضاء الطرفين غالباً ما تنتهي بحل وسط لا يرضي أحداً.

رحلة التطبيق: كيف بنينا نظامنا الجديد باستخدام CQRS

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

الخطوة الأولى: فصل النماذج الذهنية والبرمجية

أول شيء فعلناه هو التوقف عن التفكير بـ “موديل الطلب” (Order Model) الواحد. وبدأنا بالتفكير بـ:

  • أوامر الطلبات: مثل CreateOrderCommand, AddOrderItemCommand, CancelOrderCommand.
  • استعلامات الطلبات: مثل GetOrderDetailsQuery, GetUserOrderHistoryQuery.

برمجياً، هذا يعني إنشاء كلاسات منفصلة تماماً. لنأخذ مثالاً بسيطاً بلغة C#:

النموذج القديم (قبل CQRS)


// نموذج واحد لكل شيء: عرض، إنشاء، تعديل... معقد جداً!
public class Order 
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
    public string CustomerName { get; set; } // للقراءة
    public int CustomerId { get; set; } // للكتابة
    public List<OrderItem> Items { get; set; }
    public decimal TotalPrice { get; set; } // للقراءة
    
    // منطق تحقق معقد هنا...
    public void AddItem(Product product, int quantity) {
        // ...
    }
}

النماذج الجديدة (بعد CQRS)


// 1. نموذج الأمر (Command) - يركز على نية التغيير
public class CreateOrderCommand 
{
    [Required]
    public Guid CustomerId { get; set; }
    
    [MinLength(1)]
    public List<OrderItemDto> Items { get; set; }
}

// 2. نموذج الاستعلام (Query DTO) - يركز على ما سيتم عرضه
public class OrderSummaryViewModel 
{
    public Guid OrderId { get; set; }
    public string OrderDate { get; set; }
    public string CustomerName { get; set; }
    public decimal TotalAmount { get; set; }
    public int ItemCount { get; set; }
}

لاحظ كيف أن نموذج الأمر بسيط ومركز على البيانات اللازمة للتنفيذ، بينما نموذج الاستعلام “مسطح” وجاهز للعرض مباشرة في الواجهة الأمامية.

الخطوة الثانية: بناء مسار الأوامر (The Command Side)

مسار الأوامر هو المسؤول عن تنفيذ منطق العمل وتغيير الحالة. استخدمنا نمط “المُعالج” (Handler) لكل أمر. فكرة المعالج بسيطة: هو كلاس متخصص وظيفته الوحيدة هي استلام أمر معين وتنفيذه.

استخدمنا مكتبة مثل MediatR في بيئة الدوت نت لتسهيل عملية ربط الأمر بالمعالج الخاص به.


// معالج الأمر CreateOrderCommand
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand>
{
    private readonly IApplicationDbContext _writeDbContext; // اتصال بقاعدة بيانات الكتابة

    public CreateOrderCommandHandler(IApplicationDbContext writeDbContext)
    {
        _writeDbContext = writeDbContext;
    }

    public async Task<Unit> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        // 1. منطق العمل: التحقق من وجود العميل، توفر المنتجات، الخ.
        var customer = await _writeDbContext.Customers.FindAsync(request.CustomerId);
        if (customer == null) throw new Exception("العميل غير موجود");

        // 2. إنشاء الـ Aggregate Root (الكائن الرئيسي)
        var order = new Order(request.CustomerId);
        foreach(var item in request.Items)
        {
            order.AddItem(item.ProductId, item.Quantity, item.Price);
        }

        // 3. حفظ التغييرات في قاعدة بيانات الكتابة (Write DB)
        await _writeDbContext.Orders.AddAsync(order);
        await _writeDbContext.SaveChangesAsync(cancellationToken);

        // 4. لا نرجع أي بيانات! فقط إشارة بأن العملية تمت
        return Unit.Value;
    }
}

هذا المسار يتعامل مع قاعدة بيانات مُحسّنة للكتابة (Normalized Database).

الخطوة الثالثة: بناء مسار الاستعلامات (The Query Side)

هنا تكمن روعة CQRS. مسار الاستعلامات لا يعرف شيئاً عن منطق العمل المعقد. مهمته بسيطة: اقرأ من مصدر بيانات مُحسّن للقراءة وأرجع النتائج بأسرع ما يمكن.

يمكن أن يكون مصدر البيانات هذا هو نفس قاعدة البيانات، أو الأفضل من ذلك، قاعدة بيانات منفصلة ومُحسّنة للقراءة (Read Database) تكون Denormalized.


// معالج الاستعلام GetUserOrderHistoryQuery
public class GetUserOrderHistoryQueryHandler : IRequestHandler<GetUserOrderHistoryQuery, List<OrderSummaryViewModel>>
{
    private readonly IDbConnection _readDbConnection; // اتصال مباشر بقاعدة بيانات القراءة

    public GetUserOrderHistoryQueryHandler(IDbConnection readDbConnection)
    {
        // نستخدم هنا اتصال مباشر (مثل Dapper) للسرعة القصوى
        _readDbConnection = readDbConnection;
    }

    public async Task<List<OrderSummaryViewModel>> Handle(GetUserOrderHistoryQuery request, CancellationToken cancellationToken)
    {
        // استعلام SQL بسيط ومباشر ومُحسّن جداً
        // يقرأ من جدول أو View مُعد خصيصاً لهذه الشاشة
        const string sql = @"SELECT 
                                OrderId, OrderDate, CustomerName, TotalAmount, ItemCount 
                             FROM 
                                vw_OrderSummaries
                             WHERE 
                                CustomerId = @CustomerId
                             ORDER BY 
                                OrderDate DESC";
        
        var orderSummaries = await _readDbConnection.QueryAsync<OrderSummaryViewModel>(sql, new { request.CustomerId });
        
        return orderSummaries.ToList();
    }
}

لاحظ الفرق الجوهري: لا يوجد Entity Framework، لا يوجد تتبع تغييرات، لا يوجد منطق عمل. فقط استعلام مباشر وسريع.

التحدي الأكبر: مزامنة البيانات بين العالمين

هنا يأتي السؤال المنطقي: “يا أبو عمر، إذا كان لدينا قاعدة بيانات للكتابة وأخرى للقراءة، كيف نحافظ على تزامن البيانات بينهما؟”

هذا هو قلب التحدي في CQRS، والحل يكمن في تقبل مفهوم يسمى “الاتساق النهائي” (Eventual Consistency). هذا يعني أننا نقبل بأن بيانات القراءة قد لا تكون محدّثة بنسبة 100% في نفس اللحظة، ولكنها “في النهاية” ستصبح متسقة مع بيانات الكتابة. في معظم الحالات، هذا التأخير (بضع أجزاء من الثانية أو حتى ثوانٍ قليلة) مقبول تماماً.

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

  1. التحديث المزدوج (Dual Writes): يقوم معالج الأمر بالكتابة في قاعدة بيانات الكتابة ثم في قاعدة بيانات القراءة. نصيحة أبو عمر: ابعد عن الشر وغنّيله. هذه الطريقة محفوفة بالمخاطر، فماذا لو نجحت الكتابة الأولى وفشلت الثانية؟ ستدخل في دوامة عدم اتساق البيانات.
  2. مهام مجدولة (Scheduled Jobs): برنامج يعمل في الخلفية كل بضع دقائق أو ثوانٍ، يقرأ التغييرات من قاعدة بيانات الكتابة ويحدث بها قاعدة بيانات القراءة. حل بسيط لكنه يزيد من زمن التأخير.
  3. ناقل الأحداث (Event Bus) – الطريقة المثلى: وهذا ما اعتمدناه. بعد أن يقوم معالج الأمر بحفظ التغيير بنجاح في قاعدة بيانات الكتابة، يقوم بنشر “حدث” (Event) مثل OrderCreatedEvent على ناقل رسائل (مثل RabbitMQ أو Azure Service Bus). يكون هناك “مستمع” (Listener) آخر في الخلفية، وظيفته الوحيدة هي استقبال هذه الأحداث وتحديث قاعدة بيانات القراءة بناءً عليها. هذا النهج مرن وموثوق ويسمح بفصل كامل بين النظامين.

متى تستخدم CQRS ومتى تبتعد عنه؟

CQRS ليس حلاً سحرياً لكل المشاكل، وهو يضيف درجة من التعقيد للنظام. لذلك، من المهم أن تعرف متى تستخدمه.

استخدمه عندما:

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

ابتعد عنه عندما:

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

نصيحة أبو عمر: مش كل إشي بده مطرقة. لو عندك تطبيق بسيط، خليك على الـ CRUD البسيط، ما في داعي تعقّدها وتدخل في متاهات أنت في غنى عنها. ابدأ بسيطاً، وعندما تظهر المشكلة الحقيقية، وقتها فقط فكر في حلول متقدمة مثل CQRS.

خلاصة تجربة أبو عمر 💡

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

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

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

أتمنى تكون هذه التجربة مفيدة لكم. وإذا عندكم أي سؤال، أنا جاهز في التعليقات. بالتوفيق في رحلتكم البرمجية!

أبو عمر

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

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

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

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

آخر المدونات

التوظيف وبناء الهوية التقنية

سيرتي الذاتية عبرت فلتر الـ ATS لكنها فشلت أمام المدير التقني: كيف أعدت بناءها لتتحدث لغة المهندسين؟

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

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

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

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

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

لقد ‘هاجمت’ تطبيقي بنفسي عمداً: كيف كشفت لي ‘هندسة الفوضى’ نقاط الضعف التي لم تظهرها الاختبارات التقليدية

أشارككم قصة حقيقية حول إطلاق فاشل كاد أن يدمر سمعتنا، وكيف قادتنا هذه التجربة المريرة إلى تبني "هندسة الفوضى" (Chaos Engineering). اكتشفوا معنا كيف يمكن...

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

عاصفة من الطلبات كادت أن تغرق تطبيقي: كيف أنقذتني طوابير الرسائل (Message Queues) من كارثة الجمعة السوداء؟

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

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