thesis.md
3.09 KB
数据驱动的基本论点
xly 面向许多印刷行业客户销售,每个客户都希望 ERP 行为略有不同:不同表单、不同报表、不同审批规则,有时甚至不同存储过程。朴素方案是为每个客户 fork 一份代码:复制代码库、修改、部署。超过两三个客户后这就不可维护。
xly 的方案相反:单一代码库、单一部署,每客户行为用数据表达。应用中的模块、表单、字段、下拉项、权限、单据编号,甚至 URL slug,全部都是元数据表中的行(gdsmodule、gdsconfigformmaster、gdsconfigformslave、gdsroute、gdsjurisdiction、gdsformconst 等)。
运行时是一个解释器。请求到来时,框架加载相关行,叠加用户的租户上下文,并按需渲染表单 / 列表 / 报表。Java 代码是通用的;应用行为在数据库里。PM(不是工程师)拥有元数据,因此拥有应用。
成本
这个设计有三个内置成本,值得明确写出来:
-
每次请求都要读取元数据。 每次页面加载至少读取四张表(
gdsmodule、gdsconfigformmaster、gdsconfigformslave、gdsjurisdiction)以及租户过滤条件。运行时会积极缓存,但缓存未命中时这些读取不可避免。 -
schema 会持续膨胀。 新模块 =
gdsmodule中一行 +gdsconfigformslave中 1 到 50 行 + 一个支撑它的物理表(通常按单据类型)。当前实时 DB 有 901 张基础表;生产租户更多。 -
关系是约定,不是约束。 为了性能和迁移灵活性禁用外键后,从
gdsconfigformmaster.sParentId到gdsmodule.sId的连接,以及上百个类似连接,都只是语义外键。孤儿行是可能存在的。
收益
作为交换,xly 得到:
- 一个代码库服务几十个客户。 每个客户租户拥有自己的元数据行;Java 完全相同。
- PM 不占用开发时间就能演进应用。 他们打开后台、添加模块、定义表单、设置权限,下一个用户加载时即可看到变化。
- 定制可以干净地分层(切片 4):每租户覆盖叠加在共享基础之上,不需要 fork。
何时失效
数据驱动适用于客户需求能用元数据表达的情况。一旦客户需要元数据表达不了的行为,比如不同 SQL、不同存储过程主体、框架词汇无法描述的聚合规则,就会触及边界。xly 的逃生口是每客户 SQL 覆盖通道:把手写 SQL 提交到 script/客户/<customer>/,并直接应用到该客户 schema,完全绕过框架。这个通道真实存在且正在使用,但也是维护成本最高的定制方式。
这对阅读 Wiki 意味着什么
本 Wiki 中的每个切片都记录这个论点的一次应用。切片 1 是 CRUD 模块上的四表读取。切片 2 是贯穿每一层的多租户作用域。切片 3 是只读 / 视图支撑的变体。切片 4 是定制覆盖。切片 5 是覆盖不够用时的逃生口。它们合起来从中心到边界覆盖数据驱动设计。