Postgres中的版本化有限状态机

2020-08-23 13:11:20

受Felix Geisendorfer博客文章的启发,我用Postgresql实现了一个数据库FSM(有限状态机)。我对Felix的实现进行了一些改进,但在阅读以下内容之前,我建议您仔细阅读原始文章。

为简单起见,我将使用与Felix完全相同的FSM图:包含付款和发货步骤的订购流程。

首先,我将状态/事件存储从文本替换为枚举。因为我有有限数量的状态和转换,所以我可以将它们正确地存储为自定义枚举。它将这些数据的存储减少到更轻且恒定的4字节。

将类型ORDER_STATE创建为ENUM(';开始';,';等待支付';,';等待_发货';,';等待退款';,';已发运';,';已取消';,';错误';);将类型ORDER_EVENT创建为ENUM(';创建';,';支付';,';发货';,';退款';,';取消';);

创建表ORDER_EVENTS(id bigint总是作为标识主键生成,order_id UUID NOT NULL DEFAULT UUID_GENERATE_v4(),EVENT ORDER_EVENT NOT NULL DEFAULT';CREATE';,TIME TIMESTAMP DEFAULT NOW()NOT NOT。

正如您注意到的,我将order_id类型从int替换为uuid。简而言之,因为它超出了范围:我从不对从数据库中取出的字段使用SERIAL。

在他的实现中,Felix Geisendorfer使用Switch语句来实现状态转换,这是一个很好、很简单的解决方案。但是,如果您有更多的状态/事件,可能会变得很难维护。此外,如果要对FSM进行版本化,通常需要创建或替换FuncION。相反,我创建了一个包含3列的映射表:

创建表ORDER_EVENTS_TRANSIONS(STATE ORDER_STATE NOT NULL,EVENT ORDER_EVENT NOT NULL,NEXT_STATE ORDER_STATE NOT NULL,PRIMARY KEY

答:对于具有主键(状态、事件)的两个给定状态,我可以将转换限制为只有一条路径。

在我们的新转换函数中,不在此表中的任何转换都将被解析为错误状态:

CREATE Function ORDER_EVENTS_TRANSION(_STATE ORDER_STATE,_EVENT ORDER_EVENT)将ORDER_STATE语言SQL返回为$$SELECT COALESSCE((SELECT NEXT_STATE FROM ORDER_EVENTS_TRANSIONS WHERE STATE=_STATE和EVENT=_EVENT),';

安:也许有一种比“合并”更好的方式来实现“默认”…。

插入ORDER_EVENTS_TRANSFIONS值(';开始';,';创建';,';等待支付';,';等待支付';,';等待发货';),(';等待支付';,';取消';,';取消';,';取消';,';取消';,';取消';,';取消';,';取消';,';取消';),(';等待发货';,';取消';,';等待退款';,';等待发货';,';发货';,';发货';),(';等待退款';,';退款';,';取消';);

在现实生活中,我的用例和业务流程不断变化(快速)。因为我构建的FSM是这个过程的实现,所以IRL的每个更改都会影响我的数据库设计。

如果我在不考虑过去的ORDER_EVENTS的情况下更改转换函数和表,我将完全破坏我的FSM。实际上,因为我正在移动Create事件,所以所有已经记录的转换都将引发错误状态。它是不可维护的!

将类型ORDER_FSM_VERSION_STATUS创建为ENUM(';LIVE';,';已过时';,';废弃';);创建表ORDER_FSM_VERSIONS(始终作为标识主键生成的版本整数,STATUS ORDER_FSM_VERSION_STATUS NOT NULL DEFAULT';LIVE';);

CREATE函数ORDER_FSM_LAST_VERSION()以$$SELECT VERSION FROM ORDER_FSM_VERSIONS WHERE STATUS=';LIVE';ORDER BY VERSION DESC LIMIT 1;$$;

CREATE TABLE ORDER_EVENTS(id bigint总是作为标识主键生成,order_id UUID NOT NULL DEFAULT UUID_GENERATE_v4(),EVENT ORDER_EVENT NOT NULL DEFAULT';CREATE';,VERSION INTEGER NOT NULL DEFAULT ORDER_FSM_LAST_VERSION(),TIMESTAMP NOT NULL DEFAULT NOW(),外键(版本)引用ORDER_F。创建表ORDER_EVENTS_TRANSIONS(STATE ORDER_STATE NOT NULL,EVENT ORDER_EVENT NOT NULL,VERSION INTEGER NOT NULL DEFAULT ORDER_FSM_LAST_VERSION(),NEXT_STATE ORDER_STATE NOT NULL,PRIMARY KEY(STATE,EVENT,VERSION,NEXT_。

转换函数及其聚合还必须注意此版本号:

CREATE函数ORDER_EVENTS_TRANSION(_STATE ORDER_STATE,_EVENT ORDER_EVENT,_VERSION INTEGER DEFAULT ORDER_FSM_LAST_VERSION())将ORDER_STATE语言SQL返回为$$SELECT COALESSCE((SELECT NEXT_STATE FROM ORDER_EVENTS_TRANSIONS WHERE STATE=_STATE和EVENT=_EVENT AND VERSION=_。创建聚合ORDER_EVENTS_FSM(ORDER_EVENT,INTEGER)(SFUNC=ORDER_EVENTS_TRANSION,STYPE=ORDER_STATE,INITCOND=';START';);

最后,我们必须根据版本状态限制一些转换。它按ORDER_EVENTS_TRIGGER进行:

CREATE Function ORDER_EVENTS_TRIGGER_FUNC()将触发器语言plpgsql返回为$$DECLARE NEXT_STATE ORDER_STATE;TRANSPATION_STATUS ORDER_FSM_VERSION_STATUS;BEGIN SELECT STATUS。版本转换为TRANSFERATION_STATUS;如果TRANSPATION_STATUS=';已弃用';::ORDER_FSM_VERSION_STATUS,则发出通知';版本%已弃用';,新。版本;结束IF;如果转换状态=';已过时';::ORDER_FSM_VERSION_STATUS,则引发异常';版本%已过时';,新。Version;end if;select order_events_fsm(event,version order by id)from(select id,event,version from order_events where order_id=new。Order_id联合选择新建。身份证,新的。活动,新的。版本)%s进入NEXT_STATE;如果NEXT_STATE=';错误';::ORDER_STATE,则引发异常';无效订单事件';;END IF;RETURN NEW;END$$;

我现在可以引入我的FSM图的新版本,并弃用以前的版本。当我在ORDER_EVENTS中插入行时,它默认使用图形的最后一个活动版本。

插入ORDER_FSM_VERSIONS(版本,状态)值(2,';LIVE';),返回*;UPDATE ORDER_FSM_VERSIONS SET STATUS=';已弃用';WHERSION=1;INSERT INSERT INTER ORDER_EVENTS_TVERSIONS(STATE,EVENT,NEXT_STATE,VERSION)值(';开始';,';创建';,';等待批准';,2),(';等待审批';,';批准';,';等待付款';,2),(';等待付款';,';支付';,';等待发货';,2),(...)。插入ORDER_EVENTS(Order_id,Event,Time)值(';0d687d74-bab3-4c76-beed-0f55ec8a3af2';,';创建';,';2017-07-23 00:00:00';),(';0d687d74-bab3-4c76-beed-0f55ec8a3af2';,';批准';,';2017-07-23 00:00';),(';0d687d74-bab3-4c76-beed-0f55ec8a3af2';,';Pay';,';2017-07-23 12:00';);

我编写了一个小SQL脚本来测试所有这些功能。即使你不能开箱即用,我也希望它是有用的。如果您有反馈,请告诉我。

最后,请记住,它仍然是试验性的。请将其视为POC,而不是可以投入生产的产品!