软件建模03:ER 与数据建模
一台设备可以经历很多次租赁请求,其中有些被拒绝,有些曾经确认但后来取消。若只保存“设备当前属于谁”,历史请求就无法重放;若把全部确认回执都当作当前占用,取消又没有真正释放设备。建立 ER 模型时,实体和关系的选择已经影响这两种行为,远早于字段选用字符串还是整数。
结构化分析与数据流 区分了回执与有效预留的数据去向。关系模型需要进一步回答:它们各自靠什么识别,一条记录能关联多少设备,哪些关联允许暂时缺失,取消以后哪些关系仍然存在。SQL 实验可以检验其中一部分,不能替代全部租赁规则。身份来自需要区分的事实
系列基线接受指定设备、租期和请求标识,不包含完整合同。设备 E1 与 E2 即使型号相同,仍是两台不同实物;请求 A 与 B 即使设备和租期相同,仍可能代表两次不同尝试。不能用“设备加开始时刻”直接替代请求身份,否则独立尝试与同一次请求重试就被合并了。
回执以请求标识识别,因为当前契约要求同一请求 ID 重放最初结果。有效预留也使用该标识连接回执:基线一次请求只指定一台设备和一个租期,因此每份回执至多对应一条有效预留。这个选择依赖当前约束;如果一个请求能够预留三台设备,子项身份就需要重新设计。
本篇额外引入一个候选概念“参与方”,表示个人或组织。它只用于说明客户身份与请求之间的关系,尚未进入 Java 基线,也不是已经完成的认证或角色模型。参与方可以没有任何请求;为了兼容基线没有客户字段的事实,回执允许尚未关联参与方。接受匿名请求是否适合真实业务,是一个需要确认的问题。
Chen 在 1976 年论文中从可区分的实体及实体间的关联出发,说明关系也能具有属性,并讨论标识实体的键。本文用“请求、设备、参与方”这些教学材料应用该思路,没有从一篇数据库论文推导出具体租赁规则。原论文 §2
概念模型里的基数和可选性
erDiagram
PARTY o|--o{ RECEIPT : submits
EQUIPMENT ||--o{ RECEIPT : concerns
RECEIPT ||--o| BOOKING : may_have_active
图中的 PARTY、EQUIPMENT、RECEIPT、BOOKING 分别表示参与方、设备、请求回执与有效预留。使用 Mermaid 的乌鸦脚基数记法,只表达关系数量,不复刻 Chen 原文的图形符号。双竖线表示恰好一个,圆圈与竖线表示零或一个,圆圈与多叉表示零或多个。
每条回执必须指向一台设备,一台设备允许没有回执,也允许关联许多回执。每条回执可关联零或一个参与方,同一参与方可以提交多条请求。每条有效预留必须找到一条回执,回执则可以没有有效预留:被拒绝的请求从未占位,已取消的请求也不再占位。
“零或一个”没有说明缺失的原因。查询时如果发现回执没有有效预留,只能判定它目前没有占位,不能单凭这一关系区分被拒绝和已经取消。拒绝可由原结果辨认;取消发生在什么时刻、由谁操作,目前没有记录。若审计需要这些答案,应新增取消事实,不能凭空从一条已经删除的预留恢复它们。
还有一个独立约束:同一设备的有效预留租期不得重叠。它不是设备与预留之间的普通基数。设备今天和明天可以各有一条预留,总数大于一完全合法;只标注“一对一”会拒绝合法的相邻租期,而“一对多”又不能阻止重叠。关系图旁必须保留带时间条件的规则。
概念、逻辑与物理分别作出什么决定
概念模型先确定要记录的对象、关系和约束,不急着决定索引或存储引擎。逻辑模型选定关系表示:参与方与设备各一张关系,回执保存请求字段及最初结果,有效预留只保存请求标识,通过回执获取设备和租期。物理实验才选择 SQLite、具体列类型、外键开关和执行方式。
这三层是本文组织设计工作的口径。Chen 原论文 §2.1 列出的是四层,包括心智中的实体关系、信息结构、与访问路径无关及有关的数据结构,不能把今天常用的“概念、逻辑、物理”三层措辞直接归给原文。原论文 §2.1
逻辑映射减少了一份重复:booking 无需再次保存设备和租期,它所关联的回执已经有这些字段。这依赖回执保持原请求不变。Java 用不可变记录保存原请求,本篇 SQL 没有添加禁止更新回执的触发器;真实持久化实现需要通过写入接口、权限或数据库机制落实不可变契约。
| 概念决定 | 逻辑表达 | 本次物理实验 |
|---|---|---|
| 请求在当前范围内有唯一身份 | 回执键为请求 ID | PRIMARY KEY NOT NULL |
| 回执必须关联设备 | 非空设备引用 | NOT NULL REFERENCES equipment |
| 回执可暂缺参与方 | 可空参与方引用 | party_id 可为 NULL |
| 回执至多有一个有效预留 | 预留键同时引用回执 | booking.request_id 为主键及外键 |
| 取消后原请求仍可查询 | 删除预留,保留回执 | 只对 booking 执行 DELETE |
设备外键还引入了一个比 Java 基线更强的前提:数据库中必须先登记设备。Java 目前只检查设备 ID 非空白,不检查设备目录。这个差异是候选映射实验的一部分,不能把数据库的外键拒绝写成基线已有能力,更不能把增加目录检查混进一次无说明的存储替换。
用 SQLite 验证关系边界
实验使用 Python 标准库 sqlite3 和内存数据库。实际执行环境为 Python 3.14.4、SQLite 3.51.3,版本与输出一起保存在证据目录。连接建立后显式启用并读回 PRAGMA foreign_keys;SQLite 官方文档说明外键执行需要关注连接设置,不能只看到 DDL 中的 REFERENCES 就假定约束已经生效。SQLite Foreign Key Support
完整定义保存在 models/03/schema.sql。其中两个关系的连接只有下面这些内容:
1 | |
回执还保存必填设备、可选参与方、开始与结束时刻及原结果。租期使用整数时间值,并检查 start_epoch < end_epoch;实验里的 10、12、13 是同一时间轴上的简化坐标,未执行 Java Instant 的精度往返,更没有检验时区或日历计费。
1 | |
正例先建立 E1 和参与方 P1,然后写入 A、B、C 三条回执。P1 关联 A 与 B,C 暂无参与方;A 与 C 确认,B 被拒绝且不建立预留。查询断言检查一方多请求、可空参与方、拒绝回执没有占位,避免只凭建表成功就声称基数正确。
负例分别尝试引用不存在的设备、引用不存在的参与方、重复请求 ID、空租期、孤立预留、重复预留,以及删除仍被有效预留引用的回执。每条语句都必须抛出对应类别的完整性异常。意外成功,或因为另一类错误失败,都使检查程序返回非零退出码。
随后取消 A 的有效预留,逐字段比较取消前后的 A 回执,并确认 A 的预留数变为零。它验证的是“这条取消 SQL 保留原回执”。实验没有执行 Java 的重试分支,因此不能单独证明数据库适配后的重复请求仍符合全部基线契约。
可下载 本篇模型、脚本与验证记录,解压后在解压目录执行同一命令。本次执行退出码为 0,输出中的两个关键观察是:
1 | |
第二行来自有意构造的限制探针:给拒绝回执 B 插入预留,再添加与 C 重叠的已确认请求,两次写入都能通过当前约束。脚本检查记录数量后回滚到保存点。这个结果说明主外键和单行检查并没有表达跨记录的业务条件。PRAGMA foreign_key_check 返回空列表,也不能把这一限制变成业务正确性。
历史关系不等于完整审计
当前保存的历史是“最初请求及处理结果”。取消只移除活跃关系,回执还保留原设备、原租期与原结果。它足以支撑基线的重放设计,但既没有取消时间,也没有逐次修改记录。设备以后变更名称或型号,当前 schema 甚至没有这些字段;不能声称已经保存了当时展示给客户的设备说明。
如果以后允许更换设备,直接更新回执的设备 ID 会改变原请求载荷,破坏重试比较的依据。更合适的候选方案是保留请求事实,另建带有效时间的分配关系;是否需要完整版本历史,要看业务是否要求重现每次交付和变更。本篇没有提前加入分配表,只记录这一变化会使当前一请求一设备的假设失效。
| 来源或假设 | 关系模型 | 实现及证据 |
|---|---|---|
| 00 的重复请求约定 | 请求身份独立于设备与租期 | receipt 主键;重复键负例 |
| 00 的取消保留回执约定 | 有效预留关系可消失,回执保留 | 取消后逐字段比较 |
| 本篇候选假设:参与方可暂缺 | 回执到参与方为零或一 | 可空正例、未知参与方负例 |
| 本篇候选假设:先登记设备 | 回执到设备必选 | 未知设备外键负例 |
| 基线的无重叠规则 | 同设备活跃区间不相交 | 本篇 schema 未保证;限制探针明确接受重叠 |
如果目标只是保存当前单机实验结果,两个映射加显式检查仍是更简单的选择,无需为了画 ER 图就更换数据库。关系实验的价值在于把身份、缺失关联和历史保留转化成可执行的约束检查。真正把它接入业务实现时,还需要决定重试与占位的事务、并发冲突以及回执不可变性;这些问题不会由四个实体框自动解决。
参考资料
- Peter P.-S. Chen, The Entity-Relationship Model—Toward a Unified View of Data, 1976,原论文,使用 §2 的实体、关系、属性、键与层次定义。
- SQLite Foreign Key Support,用于核对外键执行与空值行为。
- 模型:
examples/software-modeling/models/03/;完整实验:examples/software-modeling/labs/03/check.py;原始输出与退出码:examples/software-modeling/evidence/03/。






