تنفيذ أجهزة الحالة في 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

  • 私が乗っていたバイクを3人の男が盗もうとしました!

    2026 年 3 月 20 日 ここ北ウェールズでは春の気配が漂っているので、昨日バイクで会社に行くことにしました。家に帰る途中、家からそれほど遠くないところで、信号待ちに座っていたところ、突然オフロードバイクに乗った3人の男が私を取り囲みました。左に 1 つ、右に 1 つ、右に 1 つが後輪まで。そして彼らは 本当に 私から数センチくらい近いです。 私はすぐに何かが起こりそうな不安を感じました。私たちは種として、このようなことに対する感覚を持っていると思います。とにかく、数秒後、私の左側の男が自転車のキーを取りに来たと思いますが、幸運なことに私はキーレスイグニッションを備えたBMWに乗っていました。 私はその男が何をしようとしているのかを観察し、パニックになり、彼の自転車の側面を全力で蹴りました。ありがたいことに、これは彼がバランスを崩して転倒するのに十分でした。それから私は右側の男の頭を殴りました。彼はヘルメットをかぶっていたので怪我はしなかったでしょうが、彼にショックを与えて数秒待つには十分だと思ったのでしょう。わかりませんが、この時点では基本的にパンツが脱げていました。 右隣の男にぶつかるやいなや、私はまったくの拍子木のように発進し、その過程で赤信号を無視しました(幸いなことに、何も来ていませんでした)。私のBMWは1000ccで170BHP以上の速いバイクです。彼らはダートバイクに乗っていたが、私のバイクほど速くはなかった。私は地元の道路についての知識も持っていましたが、そうでないことを望みました。私が飛び去ると、彼らは追いかけましたが、すぐに後退しました。スピードメーターをざっと見ただけで、時速120マイルを超えていたが、問題なく動作していた。 パニックになってどうしたらいいのかわかりませんでした – 家に帰りましょうか?もし彼らが私が車に乗り込んでいるのを見て、私の住んでいる場所を見つけたらどうしますか?どこか別の場所に行ったほうがいいでしょうか?しかし、ラッシュアワーなので、私が渋滞に巻き込まれたら、彼らは再び私に追いつく可能性があります。私の自転車は一般道では非常に速いですが、渋滞では彼らが有利になるでしょう。 私は床を床に置いて、できるだけ早く家に帰ることにしました。私の村へはまっすぐな道があるので、後ろから見えなくなったら、すぐにバイクを振り入れてガレージ(道路からは見えない)の後ろに隠れようと考えました。もしそれらが見えるなら、私はただ続けて、それらを失くそうと努力し続けるだろう。 今、車の運転に近づいているので、鏡をちらっと覗いてみましたが、何も見えませんでした。私は、子供たちがドライブで遊んでいないことを願いながら、危険を冒してグラベルドライブをできるだけ早く登ることに決めました。そうではありません。ガレージの後ろに飛び込んで待ちます…5秒…10秒…バイクが近づいてくる音が聞こえます。彼らは私の車道を通り過ぎて飛んでいき、単線の村の道路には速すぎます。 その後、妻は村のパブのオーナーに、監視カメラに何か映っていないか尋ねました。彼が私たちに見つけてくれたものは次のとおりです。 お使いのブラウザはvideoタグをサポートしていません。 そのうちの1台だけがナンバープレートを持っているようで、道路とほぼ平行なので、ビデオから特定することは不可能です。警察に通報し、法医学部門からの返答を待っています。私の自転車から何かプリントを拾えるかどうかを確認するためです。しかし、彼らが手袋をしていたかどうかは覚えていませんし、それが何かをもたらすかどうかはあまり自信がありません。 今は大丈夫ですが、とてもショックでした。彼らがもっと邪悪な何かではなく、日和見主義の愚か者であることを願うばかりです。私はすでにガレージ用にカメラを購入しました。 皆さん、外では安全に過ごしてください。 アップデート 警察の鑑識課が出動したところだ。私が彼らにビデオを見せると、彼らはバイクの跡を拭き取りました。予想通り、何も見つかりませんでした。昨日もその地域で自転車盗難未遂があり通報を受けたとのことでしたので、どうやら私だけではなかったようです。 家庭生活、バイク メールで返信 Source link

  • क्लाउड कोड कौशल बनाम वर्कफ़्लो बनाम एजेंट का उपयोग कब करें

    जब एंथ्रोपिक ने अक्टूबर 2025 में स्किल्स जारी की, तो मुझे एक भ्रमित करने वाली समस्या का सामना करना पड़ा: स्किल्स, वर्कफ़्लोज़ (पूर्व में कमांड्स), और एजेंट्स सभी अनिवार्य रूप से मार्कडाउन फ़ाइलें हैं जिनमें संकेत होते हैं। संरचनात्मक रूप से समान. तो आप किसका उपयोग करते हैं, और कब? त्वरित उत्तर प्राचीन उद्देश्य कब…

  • İNCELEME: Odyssey (Homer) – Oku Yaz Yanıtla

    Kızım son zamanlarda oynuyor DestansıHomeros’un müzikal uyarlaması Odysseytekrarlanıyor. Epic: The Musical (EPIC olarak stilize edilmiştir), tamamı Porto Rikolu aktör ve şarkıcı-söz yazarı Jorge Rivera-Herrans tarafından yazılan ve üretilen dokuz bölümlük bir albüm müzikalleri dizisidir (“destanlar” olarak anılır). 2022 ile 2024 yılları arasında yayınlanan bu müzik projesi, Antik Yunan destansı şiirinin baştan sona söylenmiş bir uyarlamasıdır….

  • Como fazer a voz de Bane

    Bane durante a famosa cena de luta subterrânea (clique em tamanho real) Não é meu tipo normal de conteúdo, obviamente, mas há uma linha específica em um filme que me deixou extremamente interessado em vozes! Eu sempre amei vozes, mas essa realmente me pegou. Foi Bane de um dos filmes do Batman. E ele disse:…

  • フーリエ変換に関する注意事項

    2026 年 7 月 14 日 20:04 タグ 数学 フーリエ級数は周期関数を解析するための優れたツールです。しかし、繰り返されない関数はどうなるでしょうか?有限区間で定義された非周期関数のフーリエ級数は、その区間を超えた動作を気にしない限り計算できることがわかりました。 このアイデアを関数に拡張してみましょう。 一度もない 繰り返す;つまり、 間隔 で定義された非周期関数です。 非繰り返し関数のフーリエ級数の視覚化 この主題を先へ進めるために、フーリエ級数に関する以前の投稿で使用された例を振り返ってみましょう。 フーリエ変換につながるフーリエ級数 これらのメモでは、フーリエ級数の複雑な指数公式を使用します。 と: 不定期のイベントに興味があります 間隔 で定義されます。したがって、 に関する上記の方程式を調べます。 まず、表記を少し変更してみます。周期 () に関して式を書く代わりに、n 次高調波の角周波数を使用します。 したがって、シリーズを次のように少し書き直すことができます。 2 つの連続する周波数の差として使用すると、次のようになります。 この表記法を使用すると、次のように表現されます。 これまでのところ、ここには新しい洞察はなく、いくつかの新しい表記があるだけです。次のステップを促進するためにこれを使用します。 それ以来 。のフーリエ級数表現の極限を計算してみます。 いつ : そして、混乱を避けるために、ダミーの積分変数を から に変更して、最新のものをこの式に代入します。 順序を少し変更し、複素指数内の で置き換えます。 和の極限を注意深く見ると、これはリーマン和です (付録 A を参照)。の「サンプル」バージョンです 、 そして 。したがって、これを積分に置き換えて、次のように変更できます。 そして次へ: 内部積分は次のように呼ばれます。 フーリエ変換 の と表されます: そして、完全な方程式は、…

Deixe um comentário

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