تنفيذ أجهزة الحالة في PostgreSQL · Felix Geisendörfer

تم النشر:
تعد آلة الحالة المحدودة (FSM) نموذجًا رائعًا للحساب وله العديد من التطبيقات العملية. أحد الأشياء المفضلة لدي هو تحويل منطق الأعمال إلى ولايات ميكرونيزيا الموحدة البسيطة. على سبيل المثال، ضع في اعتبارك المتطلبات التالية لنظام إدارة الطلبات:
- يجب ألا يتم شحن الطلبات قبل أن يتم دفعها.
- يمكن إلغاء الطلبات، ولكن فقط إذا لم يتم شحنها بعد.
- يجب استرداد الطلب الذي تم دفعه بالفعل بعد الإلغاء.
بشكل غير رسمي، تحويل قائمة المتطلبات هذه إلى ولايات ميكرونيزيا الموحدة هو ببساطة عملية التوصل إلى: مجموعة من الحالات التي يمكن أن يكون فيها النظام، ومجموعة من الأحداث، ووظيفة انتقالية تحدد مزيج كل حالة وحدث إلى حالة جديدة، وحالة أولية.
عمليًا يتم ذلك عادةً عن طريق رسم رسم بياني موجه يوضح كيفية ارتباط الحالات المختلفة ببعضها البعض عبر الأحداث:
إلى جانب توجيه تنفيذنا، يمكن أن تكون هذه الرسوم البيانية مفيدة جدًا للمناقشات والتحليل. على سبيل المثال، يمكنك أن ترى على الفور أنه لا توجد طريقة يمكن للعميل من خلالها إرجاع الطلب بعد شحنه. بالإضافة إلى ذلك، فإن عملية تسمية الدول والأحداث المعنية تخلق تلقائيًا لغة دقيقة يمكن اعتمادها من قبل جميع أصحاب المصلحة.
ربما لاحظت أن الرسم البياني أعلاه لا يعرض جميع المجموعات الممكنة لجميع الحالات وجميع الأحداث. والسبب هو أن المجموعات المفقودة تؤدي ضمنيًا إلى حالة خطأ. يمكن عرض FSM الكامل كرسم بياني أيضًا، لكنني لا أعتقد أنه مفيد جدًا في الممارسة العملية.
على أية حال، كما وعدنا في عنوان هذه المقالة، سنقوم بتنفيذ FSM في PostgreSQL. قد لا يكون هذا بمثابة كوب الشاي المفضل لدى الجميع، ولكن قد يعجبك كيف يمنح هذا النهج الذي نتبعه قوى تحليلية متقدمة كمنتج ثانوي مجاني. يمكن أن يساعد تضمين هذا النوع من المنطق في قاعدة البيانات أيضًا في الحماية من حالات العرق، ولكن ربما يكون هذا موضوعًا لمنشور مستقبلي .
لنبدأ بإنشاء ملف order_events الجدول الذي يتتبع جميع الأحداث ل معين order_id.
CREATE TABLE order_events (
id serial PRIMARY KEY,
order_id int NOT NULL,
event text NOT NULL,
time timestamp DEFAULT now() NOT NULL
);
بعد ذلك، دعونا ننفذ وظيفة النقل، والتي هي قلب كل ولايات ميكرونيزيا الموحدة:
CREATE FUNCTION order_events_transition(state text, event text) RETURNS text
LANGUAGE sql AS
$$
SELECT CASE state
WHEN 'start' THEN
CASE event
WHEN 'create' THEN 'awaiting_payment'
ELSE 'error'
END
WHEN 'awaiting_payment' THEN
CASE event
WHEN 'pay' THEN 'awaiting_shipment'
WHEN 'cancel' THEN 'canceled'
ELSE 'error'
END
WHEN 'awaiting_shipment' THEN
CASE event
WHEN 'cancel' THEN 'awaiting_refund'
WHEN 'ship' THEN 'shipped'
ELSE 'error'
END
WHEN 'awaiting_refund' THEN
CASE event
WHEN 'refund' THEN 'canceled'
ELSE 'error'
END
ELSE 'error'
END
$$;
وقبل أن نواصل، دعونا نختبر بعض الأمثلة للتأكد من أن الوظيفة تعمل:
SELECT state, event, order_events_transition(state, event)
FROM (VALUES
('start', 'create'),
('awaiting_payment', 'pay'),
('awaiting_payment', 'cancel'),
('awaiting_payment', 'ship')
) AS examples(state, event);
state | event | order_events_transition
------------------+--------+-------------------------
start | create | awaiting_payment
awaiting_payment | pay | awaiting_shipment
awaiting_payment | cancel | canceled
awaiting_payment | ship | error
يبدو ما ورد أعلاه صحيحًا، ولكن ليس من الواضح على الفور كيف يمكننا استخدام هذه الوظيفة لفرض ولايات ميكرونيزيا الموحدة الخاصة بنا على جميع الصفوف لنفس الشيء order_id في
order_events طاولة.
أول شيء نحتاجه هو طريقة جيدة لأخذ قائمة بالأحداث واستدعاء دالة الانتقال عليها بشكل متكرر لتحديد الحالة الناتجة. هناك طرق متعددة لإنجاز ذلك في PostgreSQL، ولكن ربما تكون الطريقة الأكثر أناقة هي التجميع المعرف من قبل المستخدم.
باختصار، التجميع الذي يحدده المستخدم هو دالة لها قيمة داخلية (حالة) يتم تحديثها لكل قيمة إدخال يتم تمريرها إليها باستخدام دالة انتقال الحالة. وهذا يعني أنه يناسب نموذج FSM الخاص بنا مثل القفاز:
CREATE AGGREGATE order_events_fsm(text) (
SFUNC = order_events_transition,
STYPE = text,
INITCOND = 'start'
);
ما ورد أعلاه يحدد مجموعا جديدا يسمى order_events_fsm الذي يأخذ أ
text الإدخال (أحد أحداثنا) ويستدعي order_events_transition وظيفة انتقال الحالة (FSUNC) لكل إدخال مع الحالة الحالية (STYPE) وهو أيضًا من النوع text. الحالة الأولية هي start
(INITCOND).
يُظهر الاختبار السريع أنه يعمل كما هو متوقع:
SELECT order_events_fsm(event ORDER BY id)
FROM (VALUES
(1, 'create'),
(2, 'pay'),
(3, 'cancel')
) examples(id, event);
order_events_fsm
------------------
awaiting_refund
الآن دعونا نستخدم لدينا order_events_fsm لإنشاء BEFORE INSERT الزناد لدينا order_events الجدول الذي يتأكد من جميع أحداث معينة order_id
صالحة ولا تؤدي إلى حالة خطأ. نحن نفعل هذا باستخدام بسيطة plpgsql
الوظيفة التي تنفذ لدينا order_events_fsm ضد كل الأحداث الحالية order_id بالإضافة إلى الحدث الجديد. إذا كانت الحالة النهائية error، فإننا نثير استثناءً يؤدي إلى تراجع المعاملة الحالية. يبدو مثل هذا:
CREATE FUNCTION order_events_tigger_func() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
new_state text;
BEGIN
SELECT order_events_fsm(event ORDER BY id)
FROM (
SELECT id, event FROM order_events WHERE order_id = new.order_id
UNION
SELECT new.id, new.event
) s
INTO new_state;
IF new_state = 'error' THEN
RAISE EXCEPTION 'invalid event';
END IF;
RETURN new;
END
$$;
CREATE TRIGGER order_events_trigger BEFORE INSERT ON order_events
FOR EACH ROW EXECUTE PROCEDURE order_events_tigger_func();
يمكننا التحقق من أن هذا يعمل عن طريق إدراج تسلسل صالح لأحداث الطلب:
INSERT INTO order_events (order_id, event) VALUES
(1, 'create'),
(1, 'pay'),
(1, 'ship');
INSERT 0 3
بالإضافة إلى تسلسل غير صالح للأحداث:
INSERT INTO order_events (order_id, event) VALUES
(2, 'create'),
(2, 'ship');
psql:orders.sql:95: ERROR: invalid event
CONTEXT: PL/pgSQL function order_events_tigger_func() line 14 at RAISE
كما هو متوقع، لم تصل إلى طاولتنا سوى السلسلة الأولى من الإدخالات:
SELECT id, order_id, event FROM order_events;
id | order_id | event
----+----------+--------
1 | 1 | create
2 | 1 | pay
3 | 1 | ship
إذا كنت لا تزال على الحياد بشأن تضمين هذا النوع من المنطق في قاعدة البيانات الخاصة بك، فلنرى كيف يمنحنا نهجنا صلاحيات تحليلية متقدمة كمنتج ثانوي مجاني. لنفكر في مجموعة بيانات جديدة مكونة من 3 أوامر:
TRUNCATE order_events;
INSERT INTO order_events (order_id, event, time) VALUES
(1, 'create', '2017-07-23 00:00:00'),
(1, 'pay', '2017-07-23 12:00:00'),
(1, 'ship', '2017-07-24 00:00:00'),
(2, 'create', '2017-07-23 00:00:00'),
(2, 'cancel', '2017-07-24 00:00:00'),
(3, 'create', '2017-07-23 00:00:00'),
(3, 'pay', '2017-07-24 00:00:00'),
(3, 'cancel', '2017-07-25 00:00:00'),
(3, 'refund', '2017-07-26 00:00:00');
باستخدام لدينا order_events_fsm كوظيفة نافذة، يمكننا بسهولة الحصول على سجل الحالة لأمر معين:
SELECT time, order_events_fsm(event) OVER (ORDER BY id)
FROM order_events
WHERE order_id = 3;
time | order_events_fsm
---------------------+-------------------
2017-07-23 00:00:00 | awaiting_payment
2017-07-24 00:00:00 | awaiting_shipment
2017-07-25 00:00:00 | awaiting_refund
2017-07-26 00:00:00 | canceled
ولكن يمكننا الذهاب إلى أبعد من ذلك وتطبيق أجهزة الحالة الخاصة بنا على أوامر متعددة، على سبيل المثال، باستخدام وظيفة generator_series والاستعلام الفرعي الجانبي لتقسيم عدد الطلبات لكل ولاية لكل يوم في نطاق زمني محدد:
SELECT date::date, state, count(1)
FROM
generate_series('2017-07-23'::date, '2017-07-26', '1 day') date,
LATERAL (
SELECT order_id, order_events_fsm(event ORDER BY id) AS state
FROM order_events
WHERE time < date + '1 day'::interval
GROUP BY 1
) orders
GROUP BY 1, 2
ORDER BY 1, 2;
date | state | count
------------+-------------------+-------
2017-07-23 | awaiting_payment | 2
2017-07-23 | awaiting_shipment | 1
2017-07-24 | awaiting_shipment | 1
2017-07-24 | canceled | 1
2017-07-24 | shipped | 1
2017-07-25 | awaiting_refund | 1
2017-07-25 | canceled | 1
2017-07-25 | shipped | 1
2017-07-26 | canceled | 2
2017-07-26 | shipped | 1
إليكم الأمر، تم تنفيذ FSM كتجميع محدد من قبل المستخدم في PostgreSQL مما يوفر تكامل البيانات والتحليلات المتقدمة.
ومع ذلك، قد يختلف عددك، كما أن تضمين منطق عملك في قاعدة البيانات الخاصة بك يعد دائمًا بمثابة مقايضة. ولكن إذا كنت تريد بعض الطمأنينة: لقد حققت نجاحًا كبيرًا في تطبيق هذا النهج جنبًا إلى جنب مع التجسيد المتحمس لتنفيذ لوحة معلومات تحليلية في الوقت الفعلي لتطبيق يحتوي على أكثر من مليار صف من الأحداث.
على أي حال، أنا أتطلع حقًا إلى الحصول على تعليقات حول هذا الأمر، ويسعدني جدًا الإجابة على أي أسئلة، لذا يرجى التعليق.
انضم إلي في أبل: إذا أثارت المقالة أعلاه حماسك، فتفضل بالانضمام إلى فريقي في Apple. نحن نقوم الآن بتعيين مطوري Go وPostgreSQL في شنغهاي، الصين. النقل ممكن، فقط أرسل لي رسالة بالبريد الإلكتروني لمعرفة المزيد عن هذا الدور.
شكرًا لثورستن بول ويوهانس بوين على المراجعة.
– فيليكس جايسيندورفر
اشترك في هذه المدونة عبر RSS أو البريد الإلكتروني أو احصل على تحديثات صغيرة مني عبر تغريد.
