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

حكاية فنجان الميرمية ونماذج البيانات المتضخمة

يا جماعة الخير، السلام عليكم ورحمة الله. معكم أخوكم أبو عمر.

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

في البداية، كأي مبرمج متحمس، بدأت بالبنى التقليدية. كان عندي نموذج بيانات واحد (One Model to Rule Them All)، لنقل أنه نموذج `Product`. هذا النموذج المسكين كان مسؤولاً عن كل شيء: تحديث المخزون، تسجيل المبيعات، حساب متوسط الأرباح، جلب بيانات الرسم البياني الشهري، إظهار تعليقات العملاء… كل شيء.

بعد أشهر قليلة، بدأ الكابوس. نموذج `Product` صار “متضخم” أو “Bloated” كما نقول. أصبح مثل صحن المجدرة اللي انكب فوقه كل اشي، بطلت تعرف تميّز العدس من الرز! أي تعديل بسيط في منطق الكتابة كان يهدد بكسر منطق القراءة، والعكس صحيح. الاستعلامات التي تجلب التقارير أصبحت بطيئة جداً لأنها تعمل على نفس الجداول المصممة لعمليات الكتابة السريعة. كنا نضيف فهارس (Indexes) لتسريع القراءة، فتتباطأ الكتابة. كنا نعدّل بنية الجداول لتسهيل الكتابة، فتتعقد استعلامات القراءة. دخلنا في حلقة مفرغة من “الترقيع” البرمجي.

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

ما هي المشكلة بالضبط؟ جحيم النموذج الواحد

قبل أن نغوص في تفاصيل CQRS، دعونا نحلل المشكلة التي كنت (وربما أنت الآن) أواجهها. المشكلة تكمن في استخدامنا لنموذج بيانات واحد لغرضين مختلفين تماماً:

  1. تغيير حالة النظام (الكتابة – Writes): مثل إنشاء مستخدم جديد، إضافة منتج، تحديث سعر. هذه العمليات عادةً ما تكون بسيطة، تهتم بالتحقق من صحة البيانات (Validation) وتطبيق قواعد العمل (Business Rules)، وتتطلب تناسقاً عالياً للبيانات (High Consistency).
  2. عرض حالة النظام (القراءة – Reads): مثل عرض قائمة المنتجات، توليد تقرير مبيعات، إظهار لوحة تحكم. هذه العمليات غالباً ما تكون معقدة، تتطلب تجميع بيانات من مصادر مختلفة، ولا تحتاج دائماً لأحدث البيانات “على الثانية”. الأهم فيها هو السرعة وسهولة جلب البيانات بالشكل المطلوب للعرض.

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

المنقذ: نمط CQRS على صهوة جواد أبيض

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

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

هذا الفصل ليس مجرد فصل في الكود، بل يمكن أن يمتد إلى قواعد البيانات نفسها. يمكن أن يكون لديك قاعدة بيانات للكتابة (Write DB) مصممة لتكون عالية التناسق ومُحسّنة لعمليات الإضافة والتعديل، وقاعدة بيانات أخرى للقراءة (Read DB) تكون نسخة مُبسطة أو مُحسّنة للعرض السريع.

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

كيف يعمل CQRS بالضبط؟ الفصل بين السلطات

دعونا نفصّل أكثر في الجانبين وكيف يتفاعلان معاً.

جانب الأوامر (Commands)

الأمر (Command) هو كائن بسيط يحمل النية لتغيير شيء ما في النظام. هو لا يقوم بالتغيير بنفسه، بل هو مجرد “طلب”.

  • مثال: `CreateProductCommand`, `UpdateStockCommand`.
  • الخصائص: يحتوي على كل البيانات اللازمة لتنفيذ الأمر (اسم المنتج، السعر، إلخ).
  • المُعالِج (Handler): لكل أمر هناك معالج خاص به (Command Handler). هذا المعالج يستقبل الأمر، يقوم بتطبيق كل قواعد العمل والتحقق من الصلاحيات، ثم يتفاعل مع نموذج “الكتابة” (Write Model) وقاعدة بيانات الكتابة لتحديث الحالة.
  • النتيجة: المعالج لا يُرجع بيانات. في العادة، إما أن ينتهي بنجاح (void) أو يرمي استثناءً (Exception) في حال الفشل.

جانب الاستعلامات (Queries)

الاستعلام (Query) هو أيضاً كائن بسيط يمثل طلباً للحصول على بيانات.

  • مثال: `GetProductDetailsQuery`, `GetDashboardStatsQuery`.
  • الخصائص: يحتوي على المعايير اللازمة لجلب البيانات (مثل ID المنتج، النطاق الزمني للتقرير).
  • المُعالِج (Handler): لكل استعلام معالج خاص به (Query Handler). هذا المعالج يتجاوز كل طبقات العمل المعقدة ويتجه مباشرة إلى نموذج “القراءة” (Read Model) وقاعدة بيانات القراءة لجلب البيانات بأسرع شكل ممكن.
  • النتيجة: المعالج يُرجع كائناً مخصصاً للعرض يُسمى DTO (Data Transfer Object). هذا الكائن مصمم خصيصاً ليناسب واجهة المستخدم، بدون أي بيانات إضافية غير ضرورية.

مزامنة البيانات: التناسق النهائي (Eventual Consistency)

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

الجواب يكمن في مفهوم “التناسق النهائي” (Eventual Consistency). عندما يتم تنفيذ أمر بنجاح في جانب الكتابة، يقوم النظام بإصدار “حدث” (Event)، مثل `ProductCreatedEvent`. هناك آلية (مثل Message Broker كـ RabbitMQ أو Kafka، أو حتى عملية بسيطة في الخلفية) تستمع لهذه الأحداث وتقوم بتحديث قاعدة بيانات القراءة بناءً عليها.

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

مثال عملي: من التنظير إلى الكود

دعونا نرى مثالاً بسيطاً بلغة C# (المفاهيم تنطبق على أي لغة). تخيل أننا نبني نظام إدارة منتجات.

1. تعريف الأمر (Command)


// مجرد كائن لنقل البيانات، لا منطق برمجي هنا
public class CreateProductCommand
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int InitialStock { get; set; }
}

2. معالج الأمر (Command Handler)


public class CreateProductCommandHandler
{
    private readonly IProductRepository _writeRepository;

    public CreateProductCommandHandler(IProductRepository writeRepository)
    {
        _writeRepository = writeRepository;
    }

    public void Handle(CreateProductCommand command)
    {
        // 1. التحقق من صحة البيانات وقواعد العمل
        if (command.Price <= 0)
        {
            throw new Exception("Price must be positive.");
        }

        // 2. إنشاء نموذج المجال (Domain Model)
        var product = new Product(command.Name, command.Price, command.InitialStock);

        // 3. الحفظ في قاعدة بيانات الكتابة
        _writeRepository.Add(product);
        
        // 4. (اختياري ومتقدم) نشر حدث للمزامنة
        // EventBus.Publish(new ProductCreatedEvent(product.Id, product.Name));
    }
}

3. تعريف الاستعلام (Query) ونموذج العرض (DTO)


public class GetProductByIdQuery
{
    public Guid ProductId { get; set; }
}

// DTO: كائن مخصص للعرض فقط
public class ProductViewModel
{
    public Guid Id { get; set; }
    public string ProductName { get; set; }
    public string FormattedPrice { get; set; }
}

4. معالج الاستعلام (Query Handler)


public class GetProductByIdQueryHandler
{
    // هذا يتصل مباشرة بقاعدة بيانات القراءة المحسّنة
    private readonly IReadDatabase _readDb;

    public GetProductByIdQueryHandler(IReadDatabase readDb)
    {
        _readDb = readDb;
    }

    public ProductViewModel Handle(GetProductByIdQuery query)
    {
        // استعلام بسيط جداً ومباشر من نسخة القراءة
        // قد يكون هذا جدولاً غير منمط (denormalized) أو حتى مستند NoSQL
        var productData = _readDb.Products.Find(p => p.Id == query.ProductId);

        if (productData == null) return null;

        // تحويل البيانات إلى نموذج العرض
        return new ProductViewModel
        {
            Id = productData.Id,
            ProductName = productData.Name,
            FormattedPrice = $"{productData.Price:C}" // تنسيق السعر للعرض
        };
    }
}

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

متى لا تستخدم CQRS؟ ليس كل ما يلمع ذهباً

يا جماعة، CQRS ليس حلاً سحرياً لكل المشاكل. تطبيقه يضيف درجة من التعقيد على النظام (إدارة قاعدتي بيانات، آلية المزامنة، …). لذلك، كن حذراً.

لا تستخدم CQRS إذا كان نظامك:

  • بسيطاً (CRUD App): إذا كان تطبيقك مجرد واجهات لإضافة وتعديل وحذف وعرض بيانات بسيطة، فنمط CQRS سيكون تعقيداً لا مبرر له.
  • لا يعاني من مشاكل أداء: إذا كانت عمليات القراءة والكتابة لديك متوازنة ولا يوجد أي بطء ملحوظ، فلماذا التعقيد الإضافي؟ “Don’t fix what isn’t broken”.
  • يتطلب تناسقاً قوياً وفورياً (Strong Consistency): في بعض الأنظمة (مثل الأنظمة المالية الحرجة)، قد لا يكون التأخير البسيط الناتج عن “التناسق النهائي” مقبولاً.

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

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

نمط CQRS كان بالنسبة لي أكثر من مجرد نمط معماري؛ كان تغييراً في طريقة التفكير. علمني أن أفصل بين “كيف أغير البيانات” و “كيف أرى البيانات”. هذا الفصل البسيط حررني من قيود النموذج الواحد وسمح لي ببناء أنظمة قابلة للتوسع (Scalable) وعالية الأداء وسهلة الصيانة.

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

نصيحتي الأخيرة لك: لا تخف من تحدي الطرق التقليدية في التفكير. عندما تواجه مشكلة معقدة، ابحث عن الأنماط والمفاهيم التي قد تبدو غريبة في البداية. قد تجد فيها، كما وجدت أنا في CQRS، حلاً لمشكلة تؤرقك منذ زمن.

الله يرضى عليكم ويوفقكم في مشاريعكم.

أبو عمر

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

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

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

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

آخر المدونات

تجربة المستخدم والابداع البصري

تطبيقي كان حصناً منيعاً أمام ذوي الإعاقة: كيف أنقذتني معايير الوصول الرقمي (WCAG) من جحيم الإقصاء؟

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

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

تطبيقي كان يغرق في بحر الاستعلامات: كيف أنقذني ‘التحميل المسبق’ (Eager Loading) من جحيم N+1؟

تذكرون ذلك اليوم الذي كاد فيه تطبيقي أن ينهار تحت وطأة الاستعلامات البطيئة؟ في هذه المقالة، أشارككم قصة حقيقية وكيف كانت تقنية التحميل المسبق (Eager...

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

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

كنت أغرق في بحر من الخدمات المصغرة، كل واحدة "بتغني على ليلاها". في هذه المقالة، أشارككم قصتي وكيف أصبحت بوابة الواجهات البرمجية (API Gateway) طوق...

31 مارس، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

مقابلتي التقنية كانت صمتاً مطبقاً: كيف أنقذتني ‘تقنية التفكير بصوت عالٍ’ من جحيم الرفض المحتوم؟

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

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

قاعدة بياناتي كانت على وشك الانهيار: كيف أنقذتني ‘استراتيجيات التخزين المؤقت’ (Caching) من جحيم الاستعلامات المتكررة؟

في إحدى الليالي المتأخرة، وبينما كان تطبيقي يواجه ضغطاً هائلاً كاد أن يؤدي لانهياره، اكتشفت أن الحل لم يكن في زيادة الموارد، بل في تقنية...

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