Database Engineering

بناء أنظمة عالية الإنتاجية وقابلة للتدقيق: استخدام Event Sourcing و CQRS مع PostgreSQL

في هندسة البرمجيات الحديثة، يتزايد الضغط لتحقيق التوازن بين الإنتاجية العالية للمعاملات والامتثال الصارم للمعايير التدقيقية. تواجه التطبيقات المالية ومنصات الرعاية الصحية وأنظمة إدارة المخزون متطلبات مزدوجة: يجب أن تعالج ملايين المعاملات في الثانية مع الحفاظ على سجل غير قابل للتغيير ومحمي من العبث لكل تغيير في الحالة. غالباً ما تواجه هندسات CRUD التقليدية (إنشاء، قراءة، تحديث، حذف) صعوبة في تلبية هذين المتطلبين في آن واحد، مما يؤدي إلى اختناقات في الأداء ومنطق معقد للمصالحة.

هنا تبرز قوة الجمع بين فصل مسؤولية الأوامر والاستعلامات (CQRS) وتتبع الأحداث (Event Sourcing). عند تنفيذها بشكل صحيح مع PostgreSQL، وهو محرك قاعدة بيانات علائقية قوي، يمكنك تحقيق نظام لا يكون أدائه عالياً تحت الأحمال الثقيلة فحسب، بل يكون متوافقاً بشكل جوهري مع المعايير التنظيمية. يستكشف هذا المقال أنماط التصميم المعماري، واستراتيجيات تصميم قاعدة البيانات، وتفاصيل التنفيذ العملية لبناء مثل هذه الأنظمة.

فهم الأنماط الأساسية

يغير تتبع الأحداث (Event Sourcing) الطريقة الأساسية لتخزين البيانات. بدلاً من تخزين الحالة الحالية للكيان (مثل طلب بحالة "تم الشحن")، تقوم بتخزين تسلسل من الأحداث الثابتة التي أدت إلى تلك الحالة (مثل "تم إنشاء الطلب"، "تمت معالجة الدفع"، "تم شحن البند"). لاسترجاع الحالة الحالية، يتم إعادة تشغيل هذه الأحداث. يوفر هذا النهج مسار تدقيق كاملاً بحكم تعريفه.

يفصل CQRS بين نموذج الكتابة ونموذج القراءة. في تتبع الأحداث، تكون عمليات الكتابة مكلفة لأنها تتضمن الإضافة إلى سجل قابل للإضافة فقط وتحديث الإسقاطات (Projections) محتملة. ومع ذلك، يجب تحسين عمليات القراءة لأداء الاستعلام. من خلال فصل هذه الاهتمامات، يمكنك توسيع نطاق نسخ القراءة الخاصة بك بشكل مستقل عن خوادم الكتابة، مما يضمن إنتاجية عالية بغض النظر عن تعقيد الاستعلام.

تصميم PostgreSQL لتدفق الأحداث

يعد PostgreSQL خياراً ممتازاً لتتبع الأحداث بسبب امتثاله لمعايير ACID، ودعمه القوي لـ JSONB، وتحكمه الممتاز في التزامن. عادةً ما يكون الجدول الأساسي لمخزن الأحداث سجلاً قابلاً للإضافة فقط. فيما يلي تعريف مخطط يعطي الأولوية لأداء الكتابة وسلامة البيانات.

CREATE TABLE event_store (
    id BIGSERIAL PRIMARY KEY,
    aggregate_id UUID NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    payload JSONB NOT NULL,
    version INTEGER NOT NULL,
    occurred_at TIMESTAMPTZ DEFAULT NOW()
);

-- الفهارس حاسمة للأداء
CREATE INDEX idx_event_aggregate ON event_store (aggregate_id, version);
CREATE INDEX idx_event_occurred ON event_store (occurred_at DESC);

لاحظ عمود version. هذا ضروري للتحكم في التزامن المتفائل. عند كتابة حدث، يتحقق التطبيق مما إذا كانت النسخة الحالية للتجميع (Aggregate) تطابق النسخة المتوقعة. إذا كان الأمر كذلك، يتم إضافة الحدث وزيادة النسخة. إذا لم يكن كذلك، يحدث تعارض وتفشل الكتابة، مما يمنع ظروف السباق (Race Conditions).

تنفيذ التحكم في التزامن المتفائل

تتطلب الإنتاجية العالية آليات قفل فعالة. بدلاً من قفل مستوى الصف الذي يمكن أن يسلسل عمليات الكتابة، يستفيد تتبع الأحداث من التزامن المتفائل. إليك كيفية تنفيذ كتابة معاملة في PostgreSQL باستخدام PL/pgSQL.

CREATE OR REPLACE FUNCTION append_event(
    p_aggregate_id UUID,
    p_event_type VARCHAR,
    p_payload JSONB,
    p_expected_version INTEGER
) RETURNS BIGINT AS $$
DECLARE
    v_new_version INTEGER;
    v_row_count INTEGER;
BEGIN
    -- التحقق مما إذا كان التجميع موجوداً والتحقق من صحة النسخة
    SELECT version INTO v_new_version
    FROM event_store
    WHERE aggregate_id = p_aggregate_id
    ORDER BY version DESC
    LIMIT 1;

    IF v_new_version IS NULL THEN
        v_new_version := 0;
    END IF;

    IF v_new_version != p_expected_version THEN
        RAISE EXCEPTION 'Concurrency conflict: expected version % but found %', 
                        p_expected_version, v_new_version;
    END IF;

    -- إدراج الحدث الجديد
    INSERT INTO event_store (aggregate_id, event_type, payload, version)
    VALUES (p_aggregate_id, p_event_type, p_payload, v_new_version + 1);

    RETURN v_new_version + 1;
END;
$$ LANGUAGE plpgsql;

الإسقاطات للقراءة عالية الأداء

بما أن القراءة من تدفق الأحداث الخام مكلفة حسابياً، فإن CQRS يفرض علينا الحفاظ على إسقاطات (Projections) منفصلة (أو عروض مادية) لعمليات القراءة. يتم تحديث هذه الإسقاطات بشكل غير متزامن أو متزامن مع الالتزام بالأحداث. على سبيل المثال، قد تحافظ على جدول orders_summary غير طبيعي (Denormalized) يسمح بإستعلامات سريعة حسب العميل أو التاريخ، دون الحاجة إلى إعادة تشغيل التاريخ الكامل لكل طلب.

لضمان الامتثال التدقيقي، يظل مخزن الأحداث هو مصدر الحقيقة. الإسقاطات هي بيانات مشتقة. إذا تم العثور على عدم تطابق، يمكنك إعادة بناء أي إسقاط من تدفق الأحداث الثابت، مما يضمن اتساقاً تاماً.

الخاتمة

إن تنفيذ تتبع الأحداث و CQRS مع PostgreSQL ليس حلاً سحرياً؛ فهو يقدم تعقيداً من حيث منطق التطبيق وإدارة الاتساق النهائي (Eventual Consistency). ومع ذلك، بالنسبة للأنظمة التي تتطلب إنتاجية عالية ومسارات تدقيق صارمة، فإنه يوفر أساساً معمارياً متفوقاً. من خلال الاستفادة من الميزات القوية لـ PostgreSQL للتحكم في التزامن وتخزين JSONB، يمكن للمطورين بناء أنظمة قابلة للتوسع ومتوافقة. المفتاح هو تصميم إسقاطاتك بعناية والاستثمار في معالجات أحداث قوية لإبقاء جانب القراءة متزامناً مع التاريخ الثابت المخزن في جانب الكتابة.

Share: