(总结)开发 org-zettel-ref-mode 的一点经验

最新发布的 org-zettel-ref-mode (以下简称 OZR)版本号从 0.3.3 跳到 0.4,在这一轮冲刺中,我实现了比之前更加复杂的功能,更多的代码行数,更加复杂的结构。而所花费的时间,是过去的三分之一,我认为自己取得了可喜的进步。

OZR 新的版本,增加了一个新功能,用于管理引用来源文件的可视化面板。这句话看上去很简单,但背后的工作量并不小。前端改一个词,足以让后端跑断腿。出现产品经理被砍这样的新闻,也不必奇怪了。

可视化面板,是很多产品里常见的特性,它的功能就是,让用户更加舒服地查看、管理特定的对象。在 OZR 里,其管理对象是,一个个被充当引用来源的参考文件。

如果让你来设计这个面板,你觉得第一个问题会是什么?我一开始的反映是,文件应该以什么样的方式进行展示?但实际上,在我真正着手开发时,真正重要的问题是: 在面板里展示的数据源来自哪里? 在数据源发生了改动之后,如何确保原来的文件映射关系不变?

继续追问这两个问题,会发现,从工程上需要实现:

  1. 将原本非格式化的数据转换成格式化数据,保存到数据库中,面板访问数据库才能既快又好地展现文件信息
  2. 由于用户会在面板上编辑文件信息,因此面板上更新的数据,需要同步到数据库对应的字段中,进行信息层面的对齐,这样子才能确保面板在重新加载之后不会出现错误的信息

从这 2 个问题继续追问,就会出现大量需要实现的工程细节。但最重要的一个问题是,需要使用一个怎么样的数据库,才能够满足以上需求。再深挖这个问题,就是数据库应当使用什么结构,能够满足当前的业务需求?再追问这个问题,就会回到业务层面上,实际业务的情景是什么?

设计数据库之前需要先探明业务情景

在没弄清楚实际业务发生的情形之前,我所尝试的各种实现数据库的方法,都是错的,各种 Bug 层出不穷,按下葫芦浮起瓢。最后我冷静下来,深究原因——保存与存取,总是出现问题。我得感谢之前开发的经验里,让我牢牢记住一个原则:一个 Bug 如果修改 3 次都不成功,一定要转换思路,不要继续重复原来的方法。

所以,我和 AI 对话,对实际的业务情景进行分析,寻找最适合的数据库结构。这个问题决定了数据库的读写方法——

我: overview-id 一定对应一个 ref-id (必需关系) 也未必会有对应的一个 ref-id,因为有的 overview 文件不是从 ref 源文件夹中的文件创建。不过,从严格意义上来讲,一个 overview 文件,必须对应有一个 source 文件

AI: 明白了,让我重新梳理这个关系: 从数据库角度: ref-id 可以没有对应的 overview-id overview-id 可以没有对应的 ref-id 但如果存在映射,则是一对一的关系 从业务逻辑角度: overview 文件必须对应一个 source 文件(业务规则) 但这个对应关系可能还未在数据库中建立(数据状态) 映射关系的建立是一个渐进的过程 Map 表的职责: 记录已建立的映射关系 确保映射的一对一性 不强制要求所有实体都必须有映射 但提供检查机制发现"孤立"的 overview 这样的理解更准确了吗?

我: 在我们的场景里,一个 ref-id 对应一个 overview-id。

AI: 啊,抱歉我理解错了。是的,在我们的场景中是一对一的关系。 让我们重新分析 Map 表的工作机制: 一对一映射的特点: 每个 ref-id 只能对应一个 overview-id 每个 overview-id 也只能对应一个 ref-id 这意味着映射是双向唯一的 Map 表的约束: 创建新映射前需要检查 ref-id 是否已有映射 创建新映射前需要检查 overview-id 是否已被映射 更新映射时需要保证双向唯一性 删除映射时需要同时解除双向关联 查询优化: 可以通过 ref-id 直接找到对应的 overview-id 可以通过 overview-id 反查对应的 ref-id 不需要复杂的多重映射处理 这样的理解更准确吗

我: 场景是这样的,新增加的 ref-id 未必会有对应的 overview-id。

AI: 啊,明白了。这是一个可选的一对一关系:

  1. 映射的特点:
  2. ref-id 可以没有对应的 overview-id (可选关系)
  3. 但如果有映射,则是一对一的
  4. overview-id 一定对应一个 ref-id (必需关系)
  5. Map 表的约束:
  6. 不是所有 ref-id 都需要在 Map 表中
  7. 创建映射时只需检查 overview-id 是否已被使用
  8. overview-id 不能重复使用
  9. ref-id 已有映射时,新映射会替换旧映射
  10. 业务场景:
  11. 新建 ref 时可以不立即创建 overview
  12. 可以后续再为 ref 关联 overview
  13. 可以更换 ref 对应的 overview
  14. overview 必须关联到某个 ref

这样的理解更符合实际场景了吗?

我: overview-id 一定对应一个 ref-id (必需关系) 也未必会有对应的一个 ref-id,因为有的 overview 文件不是从 ref 源文件夹中的文件创建。不过,从严格意义上来讲,一个 overview 文件,必须对应有一个 source 文件

AI: 明白了,让我重新梳理这个关系:

  1. 从数据库角度:
  2. ref-id 可以没有对应的 overview-id
  3. overview-id 可以没有对应的 ref-id
  4. 但如果存在映射,则是一对一的关系
  5. 从业务逻辑角度:
  6. overview 文件必须对应一个 source 文件(业务规则)
  7. 但这个对应关系可能还未在数据库中建立(数据状态)
  8. 映射关系的建立是一个渐进的过程
  9. Map 表的职责:
  10. 记录已建立的映射关系
  11. 确保映射的一对一性
  12. 不强制要求所有实体都必须有映射
  13. 但提供检查机制发现"孤立"的 overview

经过几轮对话之后,数据库所面对的场景,才真正的抽象出来。

实际开发时通过测试用例来 Debug

由于数据库设计的操作复杂,又是读又是取,又是加载又是保存,加上需要从数据库中读取数据,保存数据的函数很多。因此,按照以往,直接修改函数很容易遗漏。

最佳实践是,通过测试用例,来找到还没修改和完善的地方。Emacs 自带 ert 这个测试框架,可以很完善的测试各种功能。有了测试用例配合测试,就可以非常清楚地知道问题出在哪里,直接检查对应的函数。节省了大量的时间,和精力——在 Debug 过程中,最痛苦的不是改 Bug 而是找到 Bug 的源头。

3 个赞

大佬这个项目用的是什么数据库,我没看到sql,是键值数据库nosql吗?

没,打蚊子用不着大炮,就是哈希表。在哈希表里定义了数据结构。