مقدمة: عندما يعجز CRUD عن الأداء
\n\nتخيل أنك تكتب نظامًا للتداول في البورصة. كل ثانية - آلاف الأوامر. كل أمر ليس مجرد "إدراج سجل". إنه التحقق من الرصيد، إعادة حساب المحفظة، التدقيق، الإشعارات. والآن تخيل أنك لا تحتاج فقط إلى تسجيل الحالة الحالية، بل أيضًا إلى استعادة ما حدث في الساعة 14:35:22.003 أمس. CRUD العادي (إنشاء، قراءة، تحديث، حذف) يفشل هنا. مرحبًا بك في عالم CQRS و Event Sourcing في Java.
\n\nهذان النمطان ليسا مجرد كلمات رنانة. إنهما أدوات تسمح ببناء أنظمة ذات أداء عالٍ، وتدقيق كامل، وإمكانية "إعادة الزمن إلى الوراء". في هذه المقالة، سنشرح كيفية تنفيذ CQRS (فصل مسؤولية الأوامر والاستعلامات) و Event Sourcing في Java باستخدام أطر عمل حديثة وكود نظيف. بدون حشو، مع أمثلة.
\n\nما هو CQRS بكلمات بسيطة
\n\nCQRS هو فصل المسؤولية بين الأوامر (الكتابة) والاستعلامات (القراءة). في CRUD التقليدي، تستخدم نفس النموذج للقراءة والكتابة. في CQRS، تقوم بإنشاء نموذجين مختلفين: واحد لتغيير البيانات (الأوامر)، وآخر لاستخراجها (الاستعلامات).
\n\nلماذا هذا ضروري؟
\n- \n
- الأداء: يمكن توسيع نطاق القراءة والكتابة بشكل مستقل. يمكن أن يكون لديك 10 نسخ متماثلة للقراءة و2 للكتابة. \n
- الأمان: يمكن للأوامر التحقق من قواعد العمل، بينما يمكن للاستعلامات ببساطة إرجاع البيانات. \n
- المرونة: للقراءة، يمكن استخدام تمثيلات غير معيارية (denormalized)، وللكتابة، تمثيلات معيارية (normalized). \n
مثال كود: فصل الأوامر والاستعلامات
\n\nلنفترض أن لدينا نظامًا لإدارة الطلبات. بدلاً من عمل OrderService.save(order)، نقوم بالفصل:
// الأمر - هو نية لتغيير الحالة\npublic class CreateOrderCommand {\n private final String orderId;\n private final String customerId;\n private final List<OrderItem> items;\n\n // المُنشئ، الدوال الجالبة...\n}\n\n// معالج الأمر\n@Component\npublic class CreateOrderCommandHandler {\n @Autowired\n private OrderRepository orderRepository;\n\n public void handle(CreateOrderCommand command) {\n // التحقق من قواعد العمل\n if (command.getItems().isEmpty()) {\n throw new IllegalArgumentException("يجب أن يحتوي الطلب على عنصر واحد على الأقل");\n }\n Order order = new Order(command.getOrderId(), command.getCustomerId(), command.getItems());\n orderRepository.save(order);\n }\n}\n\n// الاستعلام - هو الحصول على البيانات\npublic class GetOrderQuery {\n private final String orderId;\n // المُنشئ، الدوال الجالبة...\n}\n\n// معالج الاستعلام\n@Component\npublic class GetOrderQueryHandler {\n @Autowired\n private OrderReadRepository orderReadRepository;\n\n public OrderDTO handle(GetOrderQuery query) {\n // قراءة مباشرة من قاعدة بيانات محسّنة\n return orderReadRepository.findById(query.getOrderId());\n }\n}\n\nلاحظ: OrderRepository (الكتابة) و OrderReadRepository (القراءة) هما واجهتان مختلفتان. يمكن أن يشيرا إلى قواعد بيانات مختلفة أو مخططات مختلفة.
Event Sourcing: نخزن ليس الحالة، بل التاريخ
\n\nEvent Sourcing هو نمط نقوم فيه بحفظ ليس الحالة الحالية للكائن، بل سلسلة الأحداث التي أدت إلى هذه الحالة. بدلاً من "الطلب رقم 123: الحالة = تم التوصيل" نخزن: "الحدث 1: تم إنشاء الطلب"، "الحدث 2: تمت إضافة المنتج"، "الحدث 3: تم دفع الطلب"، "الحدث 4: تم توصيل الطلب".
\n\nكيف يعمل هذا في Java
\n\nكل حدث هو كائن غير قابل للتغيير (immutable). نقوم بحفظها في Event Store (قاعدة بيانات متخصصة). يتم حساب الحالة الحالية للمجمع (aggregate) عن طريق إعادة تشغيل (replaying) جميع الأحداث.
\n\n// الحدث الأساسي\npublic interface Do