thesis.md 5.34 KB

数据驱动的基本论点

xly 面向许多印刷行业客户销售,每个客户都希望 ERP 行为略有不同:不同表单、不同报表、不同审批规则,有时甚至不同存储过程。朴素方案是为每个客户 fork 一份代码:复制代码库、修改、部署。超过两三个客户后这就不可维护。

xly 的方案相反:单一代码库、单一部署,每客户行为用数据表达。应用中的模块、表单、字段、下拉项、权限、单据编号,甚至 URL slug,全部都是元数据表中的行(gdsmodulegdsconfigformmastergdsconfigformslavegdsroutegdsjurisdictiongdsformconst 等)。

运行时是一个解释器。请求到来时,框架加载相关行,叠加用户的租户上下文,并按需渲染表单 / 列表 / 报表。Java 代码是通用的;应用行为在数据库里。PM(不是工程师)拥有元数据,因此拥有应用。

成本

这个设计有三个内置成本,值得明确写出来:

  1. 每次请求都要读取元数据。 每次页面加载在缓存未命中时会跑五类查询:gdsconfigformmaster(并为匹配的明细行叠加 personalize / customslave 覆盖)、gdsformconstsysjurisdiction(用户授权;返回 map key 叫 gdsjurisdiction,但实际读取的是 sysjurisdiction;ADMIN 会跳过)、sysbillnosettingssysreport。运行时会积极缓存,但缓存未命中时这些读取不可避免。
  2. schema 会持续膨胀。 新模块 = gdsmodule 中一行 + gdsconfigformslave 中 1 到 50 行 + 一个支撑它的物理表(通常按单据类型)。随着业务模块增加,基础表数量会继续增长;生产租户通常比干净的 dev schema 带更多表,因为客户定制模块会长期留在共享 schema 中。
  3. 关系是约定,不是约束。 为了性能和迁移灵活性禁用外键后,从 gdsconfigformmaster.sParentIdgdsmodule.sId 的连接,以及上百个类似连接,都只是语义外键。孤儿行是可能存在的。

这个设计带来的能力,以及每种能力的代价

  • 一个代码库服务几十个客户。 每个客户租户拥有自己的元数据行;Java 完全相同。——限制:它并不能覆盖所有客户。script/客户/ 下的 18 个目录(见切片 5)就是数据驱动设计撞墙的位置:当客户需要不同的过程逻辑时,“单一代码库”就变成了“单一 Java 代码库 + 一批由数据库静默承载的客户专属 SQL”。
  • PM 不占用开发时间就能演进应用。 他们打开 BACK、添加模块、定义表单、设置权限,下一个用户加载时即可看到变化。——*限制:*PM 能表达的词汇,取决于 gdsconfigformmaster / gdsconfigformslave 已经暴露的列。真正新的能力(自定义计算、非标准校验、不同保存路径)仍然需要存储过程,也就重新需要工程师,只是开发位置从 Java 换成了 SQL。没有 DB 访问权的 PM 也很难判断一次元数据改动为什么产生了错误输出,因为过程侧逻辑在 BACK 中不可见。
  • 定制可以“干净地”分层切片 4):每租户覆盖叠加在共享基础之上,不需要 fork。——限制:这种干净主要是 Java 运行时视角下的。BusinessBaseServiceImpl 中的合并逻辑本身并不简单(3,900 多行);排查“为什么这个租户能看到字段 X、看不到字段 Y”时,需要追 gdsconfigformpersonalizegdsconfigformcustomslavegdsconfigformuserslave 的组合关系。而且覆盖层不能 ALTER TABLE;真正新增物理列仍然需要协调 schema 迁移。

更直白地说:数据驱动设计把复杂度从 Java 挪到了数据库和 PM 构建的元数据里。系统总复杂度没有消失,只是转移到了框架无法编译检查的人和工具上。

何时失效

数据驱动适用于客户需求能用元数据表达的情况。一旦客户需要元数据表达不了的行为,比如不同 SQL、不同存储过程主体、框架词汇无法描述的聚合规则,就会触及边界。xly 的应对方式是每客户 SQL 覆盖通道:把手写 SQL 提交到 script/客户/<customer>/,并直接应用到该客户 schema,完全绕过框架。

这里需要说清楚:“绕过框架”意味着数据驱动论点只在系统的一部分成立。对 script/客户/ 下的 18 个客户来说,运行时已经不再是真正的单一代码库;Java 是共享的,但每个客户 DB 里实际执行的过程体会分叉,而且没有自动机制发现漂移。审查者在源码中读到的 Sp_SalSalesCheck,并不保证就是某个生产客户实际运行的版本。把它称为“逃生口”已经偏温和;实践中,覆盖通道已经变成处理重大业务逻辑差异的标准答案,而这正是数据驱动设计原本想避免的失效模式。

这对阅读 Wiki 意味着什么

本 Wiki 中的每个切片都记录这个论点的一次应用。切片 1 是 CRUD 模块上的元数据读取,也是标准实例。切片 2 是贯穿每一层的多租户作用域。切片 3 是只读 / 视图支撑的变体。切片 4 是定制覆盖。切片 5 是覆盖不够用时的逃生口。它们合起来从中心到边界覆盖数据驱动设计。