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

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


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

تم النشر:

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

  • يجب ألا يتم شحن الطلبات قبل أن يتم دفعها.
  • يمكن إلغاء الطلبات، ولكن فقط إذا لم يتم شحنها بعد.
  • يجب استرداد الطلب الذي تم دفعه بالفعل بعد الإلغاء.

بشكل غير رسمي، تحويل قائمة المتطلبات هذه إلى ولايات ميكرونيزيا الموحدة هو ببساطة عملية التوصل إلى: مجموعة من الحالات التي يمكن أن يكون فيها النظام، ومجموعة من الأحداث، ووظيفة انتقالية تحدد مزيج كل حالة وحدث إلى حالة جديدة، وحالة أولية.

عمليًا يتم ذلك عادةً عن طريق رسم رسم بياني موجه يوضح كيفية ارتباط الحالات المختلفة ببعضها البعض عبر الأحداث:

آلة الحالة المحدودة لنظام إدارة الطلبات في PostgreSQL

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

ربما لاحظت أن الرسم البياني أعلاه لا يعرض جميع المجموعات الممكنة لجميع الحالات وجميع الأحداث. والسبب هو أن المجموعات المفقودة تؤدي ضمنيًا إلى حالة خطأ. يمكن عرض 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 أو البريد الإلكتروني أو احصل على تحديثات صغيرة مني عبر تغريد.





Source link

Postagens Similares

  • Flickr 的 URL 方案-無名之輩

    我在 URL 作為使用者介面方面接受的教育有一半來自 2000 年代末的 Flickr。它的 URL 如下圖所示: flickr.com/photos/mwichary/favoritesflickr.com/photos/mwichary/setsflickr.com/photos/mwichary/sets/72177720330077904flickr.com/photos/mwichary/54896695834flickr.com/photos/mwichary/54896695834/in/set-72177720330077904 這真是令人難以置信,令人呼吸新鮮空氣。沒有多餘的 www. 在前面或尷尬 .php 在最後。沒有參數與其令人不愉快 ?&= 句法。不 % 用十六進位代碼進行聚會的標誌。當您與其他人共用這些 URL 時,您無需修改或刪除任何內容。當 Chrome 的網址列開始自動完成它們時,您就清楚地知道自己要去哪裡。 這可能看起來很愚蠢。這 使用者介面 網址數量?誰手動輸入或編輯 URL?但鍵盤仍然是最有效的輸入裝置。如果您要去的地方是您已經去過的地方,那麼輸入幾個字母可能比等待頁面加載、單擊等更快地到達目的地。它可能比篩選書籤更快到達那裡。或者,如果您要去的位置在層次結構中位於上層,則精心設計的 URL 將允許您拖曳以進行選擇,然後從末尾退格一些內容。 Flickr 允許完成這一切,而且無需按 Shift 鍵。 任何易於編輯的 URL 都需要輕鬆編輯 可讀的, 也。 Flickr 是。連結名稱非常簡單,以至於看到菜單… …準確地告訴您每個項目的 URL 是什麼。 此後的幾年裡,富文本的夢想並沒有實現。我們繼續在各地看到和使用裸 URL。這就是 Flickr URL 的另一個好處:它們很短。它們可以放在電子郵件或 Markdown 中。從頭開始,它們可以被放置在 句子。 今天,它們在 Slack 上永遠不會被中間那個令人沮喪的省略號截斷(這偶爾會導致有人複製縮短且格式錯誤的 URL 並進一步共享!)。…

  • Instantané : Viaweb, juin 1998

    janvier 2012 Quelques heures avant l’annonce de l’acquisition de Yahoo en juin 1998, j’ai pris un instantané du site de Viaweb. J’ai pensé que cela pourrait être intéressant à regarder un jour. La première chose que l’on remarque, c’est la taille des pages. Les écrans étaient beaucoup plus petits en 1998. Si je me souviens…

  • నా కొత్త స్టార్టప్ చెక్‌లిస్ట్

    నేను ఇటీవల కొత్త కంపెనీని ప్రారంభించానని మీలో కొందరికి తెలిసి ఉండవచ్చు. నేను ఇంకా -దాని గురించి మాట్లాడటానికి సిద్ధంగా లేను, కానీ నేను మొదటి సారి వ్యవస్థాపకుడిగా రాంప్ చేయాల్సిన లాజిస్టికల్ విషయాలపై కొన్ని గమనికలను సంగ్రహించాలనుకుంటున్నాను. ఇది అక్కడ ఎవరికైనా సహాయపడుతుందని ఆశిస్తున్నాము. Source link

  • Gurman Says Second-Gen iPhone Air, Coming in Early 2027, Will Sport a 0.5× Ultra-Wide Second Camera

    Mark Gurman, reporting for Bloomberg: Apple Inc. is preparing a second-generation iPhone Air for spring 2027, aiming to boost the appeal of the slimmed-down device, according to people with knowledge of the matter. Current prototypes of the new model, code-named V62, add a second rear camera for ultrawide-angle photography, said the people, who asked not…

  • விமர்சனம்: இங்கிலாந்து, இங்கிலாந்து (ஜூலியன் பார்ன்ஸ்)

    இங்கிலாந்து இங்கிலாந்து ‘வெற்று’ ஐல் ஆஃப் வைட்டில் இங்கிலாந்து தீம் பூங்காவை உருவாக்குவது பற்றிய கதை. மனிதனின் நிலம் இல்லை யாராவது? இந்த பூங்கா, காலப்போக்கில் மீண்டும் ஒன்றிணைக்கப்பட்ட கலாச்சாரம் மற்றும் வரலாற்றின் ஒரு தொகுப்பாக உள்ளது, அது ராபின் ஹூட் டி-டே விமானங்கள், அனைத்தும் தட்டையானது. மார்த்தா தனது அலுவலகத்தில் இருந்து முழு தீவையும் அனுபவிக்க முடியும். நூற்று ஒரு டால்மேஷியன்களுக்கு உணவளிப்பதை அவளால் பார்க்க முடியும், ஹவொர்த் பார்சனேஜில் த்ரோபுட்டை சரிபார்க்கவும், வைக்கோல் மெல்லும்…

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *