实体模型
实体表达身份,不可变的版本保存完整内容,引用固定到版本,只引用已保存的版本
背景
Orisumi 保存的数据来源各异:前台应用、输入事件、浏览历史、健康数据,还有模型的推断和用户的确认、否定。字段各不相同,读取方却要用同一种方式定位数据、遍历关系、按时间查找事实。
数据会变化:时段延长、应用改名、时区被纠正、字段结构演进。只保留最新值的话,观测引用的内容会在事后变样,读取方看不到观测形成时用的内容。
决定
- 所有数据都是实体,用写入方生成的 EntityId 标识。
- 实体的内容保存在版本中。每次保存形成一个不可改写的新版本,有自己的 VersionId。
- 每个实体只有一条版本链。
- 引用的值是目标的 VersionId,读到的是这个确定的版本。
- 新版本只能引用已保存且未被删除的版本。
- Core 定义 Observation、EntitySchema 和 InputSet 三种类型,其余是业务实体。
- 一个 EntitySchema 定义一种业务类型,它的每个版本是一个 Schema 版本。业务版本固定引用它所用的 Schema 版本。
- 引用的位置由 Core 或 Schema 版本声明,可以出现在顶层字段、对象成员和数组元素中。
- 只有 Observation 保存事实时间。
理由
实体和版本回答不同的问题。同一个人、同一个时段需要持续的身份;观测需要指向它当时读到的内容。两者各有身份,引用才能既锁定内容,又能按实体归集。
版本保存完整内容,读取任何一版都只需这一版。所有类型共用一种版本结构,历史机制只有一套。
只固定 Observation 本身、它引用的内容却随更新漂移,固定就没有意义,所以引用固定到版本。
只引用已保存的版本,引用就不会成环。反例:观测 A 引用尚未保存的输入集 S,观测 B 使用了 A,之后再把 B 填进 S,A 和 B 就互相依赖。写入方生成 ID,离线程序可以先在本地建立联系,再由 SDK 按依赖顺序发送。
EntitySchema 由 Core 定义,否则定义会无限递归。Observation 和 InputSet 也由 Core 定义,所有读取方按同一种方式理解观测和输入。
业务类型的身份和 Schema 版本分开后,增减字段不用新建类型,旧数据按原来的 Schema 版本解释,不需要迁移。业务含义改变时新建类型,旧数据才不会被重新解释。
引用的位置由 Core 或 Schema 版本声明,读取方不必理解业务字段,就能枚举一个版本的全部引用。
放弃的方案:
- 只保留最新值,需要历史时由各业务类型另存。 观测引用的内容会在事后改变,每种类型还要各自设计历史。
- 版本只保存差异。 读取一个版本要从首版逐版应用差异,一版出错会影响之后所有版本。
- 引用指向实体,读取时取当前版本。 追溯时看不到观测形成时用的内容。
- 允许引用目标晚到。 版本之间会出现循环依赖,后端还要在目标后到时反查引用方补做检查。
- 只允许顶层字段作为引用。 好处只是存储方便,却让物理布局限制了领域结构。
- 结构变化就新建类型和资源名。 同一业务概念的数据散落在多个类型中。
- 独立的事实时间接口。 只有 Observation 一个实现者。
代价
- 每次保存都保留完整内容,存储随版本数增长。
- 写入方必须按依赖顺序提交,两个首次出现的版本不能互相引用。
- 业务实体保存前,所用的 Schema 版本必须已保存。
- 引用可以出现在嵌套结构中,存储和反查不能只按顶层单列设计。
- 理解一份观测需要再读它引用的内容版本。