لماذا CQRS و Event Sourcing ليسا "للمختارين فقط"؟
\n\nعندما نسمع عبارتي "CQRS" أو "Event Sourcing"، يتخيل العديد من مطوري JavaScript شيئًا من عالم البنية المؤسسية في Java أو C#. مخططات معقدة، أحداث لا نهائية، مخازن غريبة... ولكن ماذا لو أخبرتك أنه يمكن (ويجب!) استخدام هذه الأنماط في تطبيقات Node.js العادية؟
\n\nفي هذه المقالة، سنفهم كيف يساعد CQRS و Event Sourcing في حل المشكلات الحقيقية: تضارب البيانات، التسجيل المعقد، التوسع، وتصحيح الأخطاء. وكل هذا بلغة JavaScript نقية، بدون سحر أو أطر عمل معقدة.
\n\nما هو CQRS (فصل مسؤولية الأوامر والاستعلامات)؟
\n\nتخيل أن تطبيقك هو مطعم. النُدُل (الاستعلامات) والطهاة (الأوامر) يعملون في نفس الغرفة. عاجلاً أم آجلاً سيبدأون في إعاقة بعضهم البعض. يقترح CQRS فصل هذه المناطق: يعمل النُدُل في الصالة (القراءة)، بينما يعمل الطهاة في المطبخ (الكتابة).
\n\nCQRS هو مبدأ فصل العمليات إلى قناتين:
\n- \n
- الأوامر (Commands) — تغير حالة النظام (إنشاء، تحديث، حذف) \n
- الاستعلامات (Queries) — تقرأ البيانات فقط (قراءة) \n
في REST API التقليدي، نستخدم عادةً نفس الكائن للقراءة والكتابة. على سبيل المثال، UserController يحتوي على دوال GET /users/:id و POST /users. يقترح CQRS فصلها إلى طبقات مختلفة.
مثال بسيط على CQRS في JavaScript
\n\n// الأمر (تغيير الحالة)\nclass CreateUserCommand {\n constructor(name, email) {\n this.name = name;\n this.email = email;\n }\n}\n\nclass UserCommandHandler {\n async handle(command) {\n // التحقق، منطق الأعمال، الحفظ\n return await db.users.insert({\n name: command.name,\n email: command.email,\n created_at: new Date()\n });\n }\n}\n\n// الاستعلام (قراءة فقط)\nclass GetUserQuery {\n constructor(userId) {\n this.userId = userId;\n }\n}\n\nclass UserQueryHandler {\n async handle(query) {\n // قراءة فقط، لا تغييرات!\n return await db.users.findOne({ _id: query.userId });\n }\n}\n\nهل يبدو أن هذا يعقد الكود؟ نعم، للوهلة الأولى. ولكن عندما ينمو تطبيقك إلى أكثر من 50 نقطة نهاية، سينقذك فصل الأوامر والاستعلامات من:
\n- \n
- تغيير البيانات بطريق الخطأ في استعلامات GET \n
- مشاكل التخزين المؤقت (من السهل تخزين الاستعلامات مؤقتًا، أما الأوامر فلا) \n
- التضارب عند العمل المتوازي لعدة مطورين \n
Event Sourcing: تذكر كل إجراء
\n\nتخيل الآن أنه بدلاً من تخزين الحالة الحالية للمستخدم، تقوم بتخزين جميع الأحداث التي حدثت له. ليس "إيفان، 25 سنة، موسكو"، بل تسلسل:
\n- \n
- "تم إنشاء المستخدم: إيفان" \n
- "قام المستخدم بتغيير العمر إلى 25" \n
- "انتقل المستخدم إلى موسكو" \n
Event Sourcing هو نمط يتم فيه حفظ كل تغيير في الحالة كحدث غير قابل للتغيير. يتم حساب الحالة الحالية عن طريق "إعادة تشغيل" جميع الأحداث.
\n\nتنفيذ مخزن الأحداث (Event Store) في JavaScript
\n\nclass EventStore {\n constructor() {\n this.events = [];\n }\n\n append(aggregateId, event) {\n this.events.push({\n aggregateId,\n event,\n timestamp: new Date()\n });\n }\n\n getEvents(aggregateId) {\n return this.events.filter(e => e.aggregateId === aggregateId);\n }\n}\n\n// استعادة الحالة من الأحداث\nfunction rebuildState(events) {\n return events.reduce((state, event) => {\n switch(event.event.type) {\n case 'USER_CREATED':\n return { ...state, name: event.event.name, age: 0 };\n case