CQRS: فصل نموذجَي القراءة والكتابة
CQRS: فصل نموذجَي القراءة والكتابة
فصل مسؤولية الأوامر عن الاستعلامات (CQRS) نمطٌ معماري يُقسّم التطبيق إلى مسارَين مستقلَّين: مسار الكتابة (الأوامر التي تُغيّر الحالة) ومسار القراءة (الاستعلامات التي تُعيد البيانات). الفكرة بسيطة لكنها قوية — شكل البيانات الذي تحتاجه لتخزينه بموثوقية نادراً ما يتطابق مع الشكل الذي تحتاجه لعرضه بكفاءة. إجبار المسارَين على نموذج واحد يُولّد احتكاكاً يتضاعف مع كل ميزة تُضيفها.
مشكلة النموذج الموحَّد
تخيّل خدمة طلبات في منصة تجارة إلكترونية. كتابة طلب تستلزم تحققاً صارماً وقواعد عمل وضمانات ACID.
لكن قراءة الطلبات لصفحة لوحة التحكم تحتاج عرضاً مُدمَجاً مسبقاً وغير مُعيَّر يحتوي أسماء المنتجات
والحالات والإجماليات — وغالباً ما يطلبها ملايين المستخدمين المتزامنين. نموذج Order الواحد
مُلزَم بتلبية الاثنين. مسار الكتابة يتباطأ لأن مسار القراءة يتطلب تحميلاً معقداً؛ ومسار القراءة
يُضيف فهارس تُعيق أداء الكتابة. تحجيم أحدهما يعني تحجيم الآخر رغم اختلاف عنق الزجاجة في كلٍّ منهما.
المفهوم الجوهري: الأوامر مقابل الاستعلامات
- الأمر (Command) — نيّة تغيير الحالة:
PlaceOrder،CancelOrder،UpdateInventory. يُتحقَّق من الأمر وينفَّذ على نموذج الكتابة (جانب الأوامر)، ثم إما ينجح أو يفشل. لا يُعيد بيانات سوى إقرار بالتنفيذ. - الاستعلام (Query) — طلب بيانات:
GetOrderSummary،ListOrdersByCustomer. يقرأ من نموذج القراءة (جانب الاستعلامات) ويُعيد DTO مُشكَّلاً بدقة للمستدعي. لا يُغيّر أي حالة أبداً.
نموذج القراءة: الإسقاطات
حين ينجح أمر ما، يُصدر حدثاً (مثل OrderPlaced). الإسقاط (Projection)
يستمع إلى هذه الأحداث ويبني عرضاً مُحسَّناً للقراءة — ربما مستنداً مُسطَّحاً في Elasticsearch
أو جدولاً مُجسَّداً في نسخة قراءة. يتحدّث نموذج القراءة بشكل غير متزامن. هذا يعني أن جانب
القراءة يمكن أن يكون غير مُعيَّر ومُدمَجاً مسبقاً ومُفهرَساً بدقة للاستعلامات
التي تحتاجها واجهة المستخدم — بدون أي ضمّ (join) وقت الاستعلام.
مثال حقيقي: في Shopify، تحتاج قوائم المنتجات إلى بيانات من جداول المنتجات والمتغيّرات والمخزون والأسعار. على جانب الكتابة، كلٌّ منها مُعيَّر. على جانب القراءة، يُجمّع إسقاطٌ مستنداً واحداً لكل قائمة حتى يكون استعلام الواجهة مجرد بحث بمفتاح-قيمة — بدون ضمّ، بكمون أقل من ميلي ثانية حتى عند ملايين الطلبات في الدقيقة.
تحجيم كل جانب باستقلالية
بما أن المسارَين منفصلان، يمكن تحجيمهما باستراتيجيات مختلفة كلياً:
- جانب الكتابة — يحتاج اتساقاً قوياً وسلامة تعاملية. يُستخدم عادةً قاعدة بيانات علائقية أساسية واحدة (PostgreSQL أو MySQL) مع تكرار متزامن. يُحجَّم رأسياً أو بالتقسيم (sharding) حسب معرّف التجميع.
- جانب القراءة — يحتاج إنتاجية عالية وكموناً منخفضاً. يمكن أن يكون ذاكرة تخزين Redis أو مجموعة نسخ قراءة أو Elasticsearch أو كتلة Cassandra. أضف نسخاً كيفما شئت؛ لا تنافس في الكتابة.
المقايضة: الاتساق النهائي
المزامنة بين مخزن الكتابة ومخزن القراءة غير متزامنة. بعد أن يُقدّم المستخدم طلباً، قد تكون هناك نافذة زمنية — عادةً من ميلي ثوانٍ إلى بضع ثوانٍ — لم يُحدَّث فيها نموذج القراءة بعد. إذا أعاد المستخدم تحميل قائمة طلباته فوراً، قد لا يرى الطلب الجديد. هذا هو الاتساق النهائي: سيتقارب النظام في نهاية المطاف، لكن ليس فوراً.
متى تستخدم CQRS
CQRS ليس الاختيار الافتراضي — فهو يُضيف تعقيداً تشغيلياً. يُجدي استخدامه حين:
- أحمال القراءة والكتابة غير متماثلة بشكل ملحوظ (مثلاً 100:1 قراءات مقابل كتابات).
- يستلزم نموذج القراءة تجميعات معقدة مكلفة الحساب وقت الاستعلام.
- تحتاج عدة تمثيلات للقراءة للبيانات ذاتها (تطبيق جوال، لوحة تحكم، خط تحليلات — لكلٍّ منها شكل مختلف).
- تستخدم بالفعل مصدر الأحداث (Event Sourcing) (الدرس التالي)، حيث الإسقاطات ملاءمة طبيعية.
CQRS عملياً: مجموعة تقنية نموذجية
إعداد شائع في الإنتاج: يستخدم جانب الكتابة PostgreSQL مع تحقق صارم من المخطط؛ تُرسَل الأوامر عبر حافلة رسائل (مثل RabbitMQ أو Kafka) وتُعالَجها معالجات تتحقق من قواعد العمل قبل التثبيت. عند النجاح، يُنشَر حدث نطاق إلى الحافلة. مستهلكو الإسقاطات (خدمات منفصلة أو عمّال غير متزامنين) يشتركون في هذه الأحداث ويُحدّثون كتلة Elasticsearch أو مجموعات مُرتَّبة في Redis تُخدّم واجهة برمجة القراءة. تُقدّم واجهة برمجة القراءة نقاط GET خفيفة لا تفعل إلا البحث في مستندات مُعدَّة مسبقاً.
نشرت فرق في Netflix وUber وAmazon تغييرات على هذه البنية. تختلف التفاصيل لكن الفصل الجوهري — الكتابة في مخزن مُعيَّر، القراءة من عرض مُصمَّم لغرض محدد — متسق في جميعها.