软件建模E01:Object-Role Modeling,从事实与约束开始
预留 R1 由 P1 提交,P2 审批了 R1。这两句话里的参与方属于同一种对象类型,但扮演不同角色。如果只画出 Party 与 Reservation 之间的一条关联,自审批是否允许、每项预留能有几个请求人,仍然没有答案。
这里的 ORM 指 Object-Role Modeling,即对象角色建模;它与把程序对象映射到关系数据库的 Object-Relational Mapping 同名缩写。前者讨论可以表达哪些事实、哪些事实组合合法,后者处理对象与表之间的存取映射。本篇实验实现有限的事实集合检查,没有运行 ORM 建模工具或数据库映射框架。
事实类型与具体事实
“预留由参与方提交”是事实类型,R1 由 P1 提交是它的一个实例。事实类型规定两个位置及其允许的对象类型,位置就是角色。同一个 Party 类型可以填入请求人位置,也可以填入审批人位置;角色的位置含义不由对象名称决定。
Halpin 的 ORM 2 记法说明以带读法的角色序列表达谓词,并用连接限定能参与该角色的对象类型。本篇只借用事实、角色和约束的概念;图采用普通流程图标记,并非完整 ORM 2 图形语法。ORM 2 Graphical Notation,第1—4页
设备租赁样本声明三个参与方、两项预留和一台设备。两项预留都由 P1 提交,都指向 E1;R1 得到 P2 审批并已确认,R2 暂未审批。这里没有给出时间区间,所以同一设备关联两项预留并不自动表示时间冲突。
flowchart LR
R["Reservation:R1、R2"] --> RB["requested_by<br/>预留由参与方提交"]
RB --> P["Party:P1、P2、P3"]
R --> FE["for_equipment<br/>预留针对设备"]
FE --> E["Equipment:E1"]
R --> AB["approved_by<br/>预留由参与方审批"]
AB --> P
C["Confirmed:R1"] --> R
第一张图把提交和审批分成两个事实类型。若它们都叫“关联参与方”,后续约束就无法说明究竟限制哪一种参与方式。模型首先要保留可读的业务谓词,然后才决定实现时采用字段、关系表还是对象引用。
本例用编号识别对象,不按显示名称判等。P1 与 P2 即使同名,也可以分别提交和审批;两个文本名称不同的记录,如果编号相同,仍代表同一个参与方。身份规则需要先于角色约束固定,否则“不许自己审批”会随去重方式变化。
至少一个与至多一个要分别表达
每项预留必须有请求人,是必选参与约束;同一项预留至多对应一个请求人,是唯一性约束。二者共同给出恰好一个。只检查已有记录是否重复,无法发现 R2 的请求人事实根本缺失。
实验依次删除 R2 的提交事实、再给 R1 增加另一个请求人。前一种输入被判为缺少必选角色,后一种输入被判为违反唯一性。两个失败使用不同错误码,便于定位缺失事实还是相互矛盾的事实。
唯一性约束放在预留一侧,并不要求参与方一生只能提交一次。样本中 P1 同时提交 R1 和 R2 是合法的。若误把请求人设置成唯一键,会拒绝正常的复购或重复申请;关系线写成“一对多”之前必须确定哪一端是哪一端。
同样的恰好一个规则用于预留与设备,但它只针对本例的一台设备预留。若业务允许一张申请包含多台设备,应先引入申请明细或重新定义事实粒度,再修改约束。不能仅把上界改成多,就继续沿用“每条预留就是一次设备承诺”的旧解释。
确认、审批和自审批不是一回事
本例增加一条教学政策:被确认的预留必须至少有一个审批人。它把 Confirmed 集合约束为审批关系在预留位置上的投影子集。R2 没有审批事实却被标记确认,实验会拒绝这个组合。
反方向并不成立。给 R2 增加审批事实,但暂不确认,仍然通过。审批与确认可能由不同步骤产生;把子集误写成集合相等,会强迫两者同时出现,消除业务过程中的中间状态。数据约束因此也会影响允许记录哪些未完成过程。
另一条政策禁止同一个人提交并审批同一项预留。实现比较 requested_by 与 approved_by 两个关系中的完整二元组。R1 的请求人为 P1,增加“R1 由 P1 审批”会失败;P1 在别的预留里担任审批人,不会仅因其曾提交过申请而被禁止。
审批人没有至多一个限制。给 R1 增加 P3 的审批,原来的 P2 审批仍保留,样本继续合法。这是本篇明确采用的政策,并不是所有设备租赁系统的通用规定。若需要双人复核、顺序审批或审批撤销,应另外增加人数、顺序与时间方面的事实。
flowchart TB
INPUT["事实集合"] --> TYPE["对象类型及编号存在"]
TYPE --> COUNT["每项预留:请求人和设备恰好一个"]
COUNT --> SUB["确认集合包含于已审批预留集合"]
SUB --> EX["同一预留的提交人、审批人不得相同"]
EX --> OK["当前样本合法"]
TYPE --> F["带约束名称的失败"]
COUNT --> F
SUB --> F
EX --> F
第二张图对应检查顺序。先排除未知参与方,再计算角色数量,避免把拼错的编号算成有效审批人。它是面向当前有限样本的检查程序,不是解释任意 ORM 约束语言的引擎,也没有搜索所有可能事实集合。
映射到 ER 时保留哪些信息
通过验证的事实可以整理成预留记录:编号、请求人编号、设备编号、审批人集合与确认标记。实验导出的 R2 记录保留空审批集合,表明暂未审批;若使用必须匹配审批记录的内连接,R2 会从结果中消失,未完成申请也就不可见。
这个映射没有要求所有事实各占一张表。单值且必选的请求人可以成为外键字段,多值审批可以放在关联表,确认状态也可以由其他机制维护。最终存储方案要逐条核对约束是否仍能表达,不能从“概念图画了几条边”直接推出表的数量。
关系数据库中的外键和非空约束能承担部分检查,自审批排斥或确认必须有审批可能需要额外机制。对象关系映射框架可以帮助装载这些记录,但使用框架本身不会自动补出本篇的业务约束。两个 ORM 缩写涉及的工作可以相接,却不能互相替代。
事实集合按集合语义处理。把完全相同的“R1 由 P1 提交”输入两遍,实验导出的业务记录不变。这只说明同一命题没有新增含义;若要记录两次提交尝试,就需要尝试编号或发生时间,使两次事件成为不同事实,不能依靠重复行隐式计数。
三元事实不能随意拆成三条二元关系
另一个小样本记录“参与方在某日领取某设备”。已知 P1 在 D1 领取 E1、P1 在 D2 领取 E2、P2 在 D2 领取 E1。每条事实的三个角色共同限定发生了什么,任意两项只能表达部分信息。
实验分别投影出人和设备、人和日期、设备和日期,再连接这三组二元事实。结果额外出现“P1 在 D2 领取 E1”。三个局部配对都存在,但原始三元集合中没有这条事实;拆分之后丢失了哪些值属于同一次领取的信息。
这个反例并不宣称三元关系永远不能分解。分解是否保持信息取决于具体依赖和约束;某些额外业务规则可能排除多义组合。本例没有那些规则,因此不能根据较简单的二元图就断言重建无损。检查输出单独保存了原始集合、连接结果和新增的虚假事实。
约束的范围也属于模型
R2 没有审批人时,样本记录的是尚未出现审批事实,不是在 Party 集合中增加一个叫“未知”的参与方。用占位编号填满所有关联,会让必选与可选的区别消失;占位者还可能被误计入审批人数。因此,缺少事实与存在一个值为空的事实需要分别定义。
这里的角色是谓词中的位置,并不要求程序为每个位置创建一个 RoleObject。某些角色需要有效期和独立状态,适合成为对象;某些只是一个稳定关联的端点。是否对象化取决于需要记录的身份与变化,不能仅凭方法名称带有“角色”就统一套用类结构。
本例检查的是一个时刻提交的完整样本。在逐步编辑过程中,先建立 R3 再填请求人可能暂时违反必选约束;工具可以允许草稿不完整,但最终验收仍需执行全部约束。把编辑中间态和已接受模型分开,可以避免为了方便录入而永久放松业务规则。
复跑与适用范围
1 | |
Python 标准库实验实际运行 13 个场景,全部通过。这里的通过包括正确检出负例:必选角色缺失、请求人不唯一、未知参与方、无审批确认、自审批,以及三元分解产生虚假事实。evidence/E01/er-rows.json 保存映射记录,decomposition.json 保存信息损失对照。






