في مشهد هندسة قواعد البيانات الحديثة، لم يكن الطلب على مسارات البيانات غير القابلة للتعديل (Immutable) أعلى من أي وقت مضى. سواء كان ذلك للامتثال المالي، أو تصحيح الأخطاء في انتقالات الحالة المعقدة، أو إعادة بناء سلوك النظام بعد انقطاع الخدمة، فإن لقطات "الحالة الحالية" التقليدية غالباً ما تفشل في تلبية هذه المتطلبات. هنا يأتي دور Event Sourcing ليبرز. من خلال التقاط كل تغيير في الحالة كسلسلة من الأحداث، نحول قاعدة البيانات من مجرد آلية تخزين إلى دفتر حسابات شامل وموثق ذاتياً لتاريخ النظام.
في حين يرتبط Event Sourcing غالباً بالهندسة القائمة على الأحداث باستخدام وسائط الرسائل مثل Kafka، فإن أساس هذا النمط هو قاعدة البيانات. يقدم PostgreSQL ميزة فريدة هنا بفضل ضمانات ACID القوية، ودعم JSONB القوي، والقدرة الأصلية على التعامل بكفاءة مع أحمال العمل التي تعتمد على الإضافة فقط (Append-only). في هذا المقال، سنستكشف كيفية هندسة طبقة Event Sourcing باستخدام PostgreSQL لضمان موثوقية مطلقة لسجل التدقيق.
الفلسفة الأساسية: الإضافة فقط هي القاعدة الإلزامية
القاعدة الأساسية في Event Sourcing هي أنه يجب إضافة الأحداث فقط، ولا يجوز تعديلها أو حذفها. إذا قمت بتعديل حدث، فإنك تكسر سلسلة السببية. في PostgreSQL، يتحقق ذلك عادةً عن طريق هيكلة المخطط (Schema) ليعتمد على عمود هوية (عادةً serial أو bigserial كمفتاح رئيسي) يزداد بشكل رتيب. يعتمد ترتيب الأحداث في حالة التطبيق بالكامل على الترتيب الذي تم به كتابتها في قاعدة البيانات، وهو ما تضمنه سجلات المعاملات في قاعدة البيانات (WAL).
لضمان الموثوقية، يجب فرض عدم القابلية للتعديل على مستوى قاعدة البيانات. بينما يمكن لمنطق التطبيق منع التعديلات، يجب أن تكون قاعدة البيانات هي السلطة النهائية. نحقق ذلك عن طريق إنشاء قيود (Constraints) أو استخدام المحفزات (Triggers)، على الرغم من أن المخطط المصمم جيداً غالباً ما يجعل القيود الصريحة غير ضرورية إذا كانت طبقة الوصول صارمة.
تصميم مخطط لتدفقات الأحداث
يتطلب تصميم المخطط الموازنة بين أداء الاستعلام وطبيعة الكتابة الكثيفة لـ Event Sourcing. يتضمن النهج القياسي جدولاً يعمل كمخزن للأحداث. تشمل الأعمدة الرئيسية معرّف حدث فريد، ومعرف تدفق (لتجميع الأحداث ذات الصلة)، ونوع الحدث، والحمولة (غالباً JSONB للمرونة)، ورقم إصدار للقفز التحفظي (Optimistic Locking).
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
stream_id UUID NOT NULL,
event_type VARCHAR(255) NOT NULL,
version INTEGER NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT unique_event_version
UNIQUE (stream_id, version)
);
-- إنشاء فهرس على stream_id و version لقراءة التدفقات بكفاءة
CREATE INDEX idx_events_stream_version
ON events (stream_id, version);
-- إنشاء فهرس على event_type للبحث العالمي عبر جميع التدفقات
CREATE INDEX idx_events_type
ON events (event_type);
عمود payload باستخدام JSONB أمر بالغ الأهمية. فهو يسمح لنا بتخزين بيانات أحداث غير متجانسة (مثل OrderCreated مقابل PaymentFailed) دون الحاجة إلى أعمدة SQL صلبة لكل حقل. يسمح نوع JSONB في PostgreSQL أيضاً بالاستعلام عن سمات محددة داخل حمولة الحدث بكفاءة باستخدام فهارس GIN إذا لزم الأمر.
كتابة الأحداث مع القفز التحفظي (Optimistic Locking)
الموثوقية في Event Sourcing لا تتعلق فقط بإضافة البيانات، بل تتعلق بضمان الاتساق عند محاولة عمليات متعددة تحديث نفس التدفق. هنا يصبح التحكم في التزامن التحفظي (OCC) حيوياً. نستخدم عمود version لمنع فقدان التحديثات.
عند حفظ حدث جديد، يحسب التطبيق الإصدار المتوقع (عادةً last_known_version + 1). لا تتم الموافقة على معاملة قاعدة البيانات إلا إذا كان الصف الحالي الذي يحمل هذا stream_id و version لا يزال مطابقاً. إذا تدخلت عملية أخرى، يفشل فحص الإصدار، وتتم إعادة المعاملة (Rollback).
-- SQL شبهي (Pseudo-SQL) لحفظ حدث مع فحص الإصدار
INSERT INTO events (stream_id, event_type, version, payload, created_at)
VALUES (
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
'OrderCreated',
5,
'{"orderId": 123, "total": 99.99}',
NOW()
)
ON CONFLICT (stream_id, version)
DO NOTHING;
-- يجب على التطبيق التحقق مما إذا كان الإدراج قد نجح.
-- إذا فشل بسبب التعارض، يجب إعادة المحاولة في المعاملة.
يضمن هذا الآلية عدم قدرة عمليتين متزامنتين على فسخ تسلسل الأحداث. إذا حدث تعارض، يمكن للتطبيق استرجاع أحدث حالة للتدفق وإعادة المحاولة، مما يعالج ظروف السباق (Race Conditions) بفعالية.
الاستعلام عن التاريخ وإعادة بناء الحالة
إحدى أقوى جوانب هذا الهندسة هي القدرة على إعادة بناء الحالة الحالية للكائن عن طريق إعادة تشغيل الأحداث من البداية (أو من لقطة). بما أن PostgreSQL يخزن الأحداث بترتيب صارم بناءً على المفتاح الرئيسي، يمكننا قراءة تاريخ التدفق بشكل موثوق.
-- جلب جميع الأحداث لتدفق معين بالترتيب الزمني
SELECT payload
FROM events
WHERE stream_id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'
ORDER BY version ASC;
في الأنظمة عالية الحجم، فإن قراءة كل حدث من بداية الزمن غير فعالة. النمط الشائع هو الحفاظ على "لقطة" (Snapshot) للحالة الحالية، وتخزينها في جدول منفصل. عند إعادة التشغيل، يقوم النظام بتحميل أحدث لقطة ثم يطبق فقط الأحداث التي حدثت بعد تلك النقطة. يوفر هذا النهج الهجين أفضل ما في العالمين: موثوقية Event Sourcing لسجلات التدقيق وأداء تخزين الحالة التقليدي.
ضمان موثوقية سجل التدقيق
لاستغلال PostgreSQL بشكل حقيقي في سجلات التدقيق، يجب تكوين قاعدة البيانات بشكل صحيح. تأكد من تكوين فتحات النسخ (Replication Slots) لمنع مشاكل أرشفة السجلات إذا كنت تستخدم الاسترداد في نقطة زمنية محددة (Point-in-time recovery). بالإضافة إلى ذلك، يوفر سجل الكتابة المسبقة (WAL) في PostgreSQL آلية لاستعادة قاعدة البيانات إلى أي نقطة زمنية، وهو ما يعمل كاحتياطي نهائي لتدفق الأحداث الخاص بك.
من أجل الامتثال، ضع في اعتبارك تمكين تشفير مستوى الجدول أو استخدام pgcrypto لتشفير الحقول الحساسة داخل عمود payload، مما يضمن أن سجل التدقيق نفسه آمن.
الخلاصة
يؤدي تنفيذ Event Sourcing مع PostgreSQL إلى تحويل قاعدة البيانات إلى سجل تدقيق موثوق وغير قابل للتعديل. من خلال الالتزام الصارم بأنماط الإضافة فقط، والاستفادة من JSONB في PostgreSQL للحمولات المرنة، وفرض القفز التحفظي على الإصدارات، فإنك تنشئ نظاماً يكون قوياً وشفافاً في آن واحد. بينما يقدم هذا النمط تعقيداً في القراءة وإعادة بناء الحالة، فإن الفوائد - وهي إمكانية التتبع، وسهولة التصحيح، والامتثال - لا تقدر بثمن للتطبيقات الحديثة والحرجة.
مع تقدمك، تذكر أن Event Sourcing هو أداة لإدارة التعقيد، وليست حلاً سحرياً. استخدمه حيث يكون تاريخ التغييرات بنفس أهمية الحالة الحالية، ودع PostgreSQL يقوم بالعمل الشاق لضمان عدم ضياع هذا التاريخ أبداً.