أصبح التصميم القائم على الأحداث (Event Sourcing) نمطاً معمارياً قوياً في الأنظمة الموزعة الحديثة، حيث يوفر سجل تدقيق غير قابل للتغيير ويمكّن من استعلامات زمنية معقدة. ومع ذلك، فإن تنفيذه بفعالية يطرح تحديات كبيرة، لا سيما فيما يتعلق بإدارة الإسقاطات (Projections) والتعامل مع تطور المخطط (Schema Evolution). وعلى عكس تطبيقات CRUD التقليدية حيث يتم تعديل البيانات في مكانها، يتطلب التصميم القائم على الأحداث إعادة بناء الحالة من تيار من الأحداث، مما يجعل الفصل بين الأوامر والأحداث والإسقاطات أمراً بالغ الأهمية.
التحدي الجوهري: الأحداث غير القابلة للتغيير مقابل الإسقاطات القابلة للتغيير
في النظام المعتمد على الأحداث، يُعد تسلسل الأحداث هو "مصدر الحقيقة". يجب أن تبقى هذه الأحداث غير قابلة للتغيير للحفاظ على سلامة تاريخ النظام. ومع ذلك، يجب أن تتطور المشاهدات الإسقاطية—وهي نماذج القراءة التي تستهلكها الواجهة الأمامية أو واجهة برمجة التطبيقات (API)—مع تغير متطلبات العمل. يخلق هذا التناقض مشكلة هندسية فريدة: كيف يمكنك تحديث نماذج القراءة دون إعادة كتابة التاريخ أو التسبب في عدم اتساق البيانات أثناء التحديثات؟
تكمن الحل في معاملة الإسقاطات ككيانات أولى الدرجة في نموذج البيانات الخاص بك. فهي ليست مجرد جداول قاعدة بيانات، بل مكونات مميزة يجب أن تكون ذات إصدارات، وقابلة للهجرة، وقابلة لإعادة البناء في بعض الأحيان.
التعامل مع تطور المخطط في الأحداث
أحد أكثر المشكلات استمراراً في التصميم القائم على الأحداث هو انحراف المخطط (Schema Drift). إذا تطورت تطبيقك ليشمل حقولاً جديدة في حدث، فكيف تضمن التوافق مع الإصدارات السابقة مع الخدمات القديمة التي لم يتم تحديثها بعد؟
القاعدة الذهبية هي: لا تغير أبداً اسم أو نوع خاصية حدث موجودة. أضف دائماً خصائص جديدة. إذا اضطررت لتغيير خاصية، فأنشئ نوع حدث جديد. لتسهيل ذلك، يجب أن يتضمن مخطط الحدث الخاص بك رقم إصدار.
خذ في الاعتبار حدث إنشاء طلب بسيط. في حالته الأولية، قد يبدو هكذا:
{
"eventId": "123e4567-e89b-12d3-a456-426614174000",
"aggregateId": "order-001",
"timestamp": "2023-10-27T10:00:00Z",
"type": "OrderCreated",
"version": 1,
"customerId": "cust-123",
"totalAmount": 150.00,
"currency": "USD"
}
في وقت لاحق، إذا احتجت إلى تتبع عنوان الشحن في وقت إنشاء الطلب، فلا يجب تعديل الحدث الموجود. بدلاً من ذلك، قد تقدم نوع حدث جديد أو تضيف حقلاً قابلاً للقيمة الفارغة (nullable) مع الحفاظ على دعم الهيكل القديم في منطق فك التشفير (deserialization). يمكن استخدام مكتبة مثل Newtonsoft.Json أو System.Text.Json مع سمات ignoreDataMember للمساعدة في إدارة هذه الاختلافات بسلاسة.
تصميم إسقاطات قوية
الإسقاطات هي الآلية التي نحول بها الأحداث غير القابلة للتغيير إلى نماذج قراءة قابلة للاستعلام. يجب أن تكون قابلة للتكرار (Idempotent) ومعزولة عن منطق المجال الذي ينتج الأحداث.
يجب أن يقوم معالج الإسقاط المصمم جيداً بما يلي:
- الاشتراك في أنواع أحداث محددة.
- تحويل بيانات الحدث إلى تنسيق مناسب لقاعدة بيانات القراءة (غالباً ما تكون قاعدة بيانات علائقية أو مستندات).
- التعامل مع الأحداث المفقودة أو غير المرتبة زمنياً بسلاسة.
إليك مثالاً مفهوماً لمعالج الإسقاط في C#:
public class OrderTotalProjection : IHandleEvents<OrderCreated>, IHandleEvents<OrderCancelled>
{
private readonly IDocumentSession _session;
public OrderTotalProjection(IDocumentSession session)
{
_session = session;
}
public void Handle(OrderCreated @event)
{
var projection = new OrderProjection
{
OrderId = @event.AggregateId,
Status = "Open",
Total = @event.TotalAmount
};
// منطق الإدراج أو التحديث لضمان قابلية التكرار
_session.Store(projection);
}
public void Handle(OrderCancelled @event)
{
var projection = await _session.LoadAsync<OrderProjection>(@event.AggregateId);
if (projection != null)
{
projection.Status = "Cancelled";
projection.Total = 0;
_session.Store(projection);
}
}
}
هجرة الإسقاطات في بيئة الإنتاج
عندما يتغير مخطط نموذج القراءة الخاص بك—على سبيل المثال، إضافة حقل shippingAddress إلى OrderProjection—لا يمكنك ببساطة تشغيل هجرة لقاعدة البيانات. يجب عليك أيضاً إعادة بناء بيانات الإسقاط من تيار الأحداث. يتم القيام بذلك عادةً عن طريق:
- نشر كود الإسقاط الجديد جنباً إلى جنب مع الكود القديم.
- تشغيل عامل خلفية (background worker) يعيد تشغيل جميع الأحداث للكيانات المتأثرة.
- تحديث قاعدة بيانات القراءة بالمخطط الجديد.
- تبديل واجهة برمجة التطبيقات (API) للاستعلام عن العرض الجديد بمجرد اكتمال ملء البيانات الناقصة (backfill).
الخاتمة
يتطلب نمذجة البيانات للتصميم القائم على الأحداث تحولاً في العقلية. لم تعد تصمم الجداول للتخزين؛ بل تصمم تيارات للتاريخ وواجهات للوصول. من خلال الفصل الصارم بين تطور الأحداث وتحديثات الإسقاط، وضمان أن تكون الإسقاطات قابلة للتكرار وذات إصدارات، يمكنك بناء أنظمة موزعة لا تكون مرنة فحسب، بل تحافظ أيضاً على تاريخ واضح وقابل للتدقيق لجميع الأنشطة التجارية. احتضن التعقيد، وسيجعل نظامك ذلك يستحق العناء من خلال مرونة وقابلية تتبع لا مثيل لهما.