# 数据驱动的基本论点 xly 面向许多印刷行业客户销售,每个客户都希望 ERP 行为略有不同:不同表单、不同报表、不同审批规则,有时甚至不同存储过程。朴素方案是为每个客户 fork 一份代码:复制代码库、修改、部署。超过两三个客户后这就不可维护。 xly 的方案相反:**单一代码库、单一部署,每客户行为用数据表达**。应用中的模块、表单、字段、下拉项、权限、单据编号,甚至 URL slug,全部都是元数据表中的行(`gdsmodule`、`gdsconfigformmaster`、`gdsconfigformslave`、`gdsroute`、`gdsjurisdiction`、`gdsformconst` 等)。 运行时是一个*解释器*。请求到来时,框架加载相关行,叠加用户的租户上下文,并按需渲染表单 / 列表 / 报表。Java 代码是通用的;应用行为在数据库里。PM(不是工程师)拥有元数据,因此拥有应用。 ## 成本 这个设计有三个内置成本,值得明确写出来: 1. **每次请求都要读取元数据。** 每次页面加载在缓存未命中时会跑五类查询:`gdsconfigformmaster`(并为匹配的明细行叠加 personalize / customslave 覆盖)、`gdsformconst`、`sysjurisdiction`(用户授权;返回 map key 叫 `gdsjurisdiction`,但实际读取的是 `sysjurisdiction`;ADMIN 会跳过)、`sysbillnosettings`、`sysreport`。运行时会积极缓存,但缓存未命中时这些读取不可避免。 2. **schema 会持续膨胀。** 新模块 = `gdsmodule` 中一行 + `gdsconfigformslave` 中 1 到 50 行 + 一个支撑它的物理表(通常按单据类型)。随着业务模块增加,基础表数量会继续增长;生产租户通常比干净的 dev schema 带更多表,因为客户定制模块会长期留在共享 schema 中。 3. **关系是约定,不是约束。** 为了性能和迁移灵活性禁用外键后,从 `gdsconfigformmaster.sParentId` 到 `gdsmodule.sId` 的连接,以及上百个类似连接,都只是[语义外键](semantic-fk.md)。孤儿行是可能存在的。 ## 这个设计带来的能力,以及每种能力的代价 - **一个代码库服务几十个客户。** 每个客户租户拥有自己的元数据行;Java 完全相同。——*限制:*它并不能覆盖所有客户。`script/客户/` 下的 18 个目录(见[切片 5](../slices/05-customer-sql-override.md))就是数据驱动设计撞墙的位置:当客户需要不同的过程逻辑时,“单一代码库”就变成了“单一 Java 代码库 + 一批由数据库静默承载的客户专属 SQL”。 - **PM 不占用开发时间就能演进应用。** 他们打开 BACK、添加模块、定义表单、设置权限,下一个用户加载时即可看到变化。——*限制:*PM 能表达的词汇,取决于 `gdsconfigformmaster` / `gdsconfigformslave` 已经暴露的列。真正新的能力(自定义计算、非标准校验、不同保存路径)仍然需要存储过程,也就重新需要工程师,只是开发位置从 Java 换成了 SQL。没有 DB 访问权的 PM 也很难判断一次元数据改动为什么产生了错误输出,因为过程侧逻辑在 BACK 中不可见。 - **定制可以“干净地”分层**([切片 4](../slices/04-custom-field.md)):每租户覆盖叠加在共享基础之上,不需要 fork。——*限制:*这种干净主要是 Java 运行时视角下的。`BusinessBaseServiceImpl` 中的合并逻辑本身并不简单(3,900 多行);排查“为什么这个租户能看到字段 X、看不到字段 Y”时,需要追 `gdsconfigformpersonalize`、`gdsconfigformcustomslave`、`gdsconfigformuserslave` 的组合关系。而且覆盖层不能 `ALTER TABLE`;真正新增物理列仍然需要协调 schema 迁移。 更直白地说:数据驱动设计把复杂度从 Java 挪到了数据库和 PM 构建的元数据里。系统总复杂度没有消失,只是转移到了框架无法编译检查的人和工具上。 ## 何时失效 数据驱动适用于客户需求能用元数据表达的情况。一旦客户需要元数据表达不了的行为,比如不同 SQL、不同存储过程主体、框架词汇无法描述的聚合规则,就会触及边界。xly 的应对方式是[每客户 SQL 覆盖通道](../slices/05-customer-sql-override.md):把手写 SQL 提交到 `script/客户//`,并直接应用到该客户 schema,完全绕过框架。 这里需要说清楚:“绕过框架”意味着数据驱动论点只在系统的一部分成立。对 `script/客户/` 下的 18 个客户来说,运行时已经不再是真正的单一代码库;Java 是共享的,但每个客户 DB 里实际执行的过程体会分叉,而且没有自动机制发现漂移。审查者在源码中读到的 `Sp_SalSalesCheck`,并不保证就是某个生产客户实际运行的版本。把它称为“逃生口”已经偏温和;实践中,覆盖通道已经变成处理重大业务逻辑差异的标准答案,而这正是数据驱动设计原本想避免的失效模式。 ## 这对阅读 Wiki 意味着什么 本 Wiki 中的每个切片都记录这个论点的一次*应用*。切片 1 是 CRUD 模块上的元数据读取,也是标准实例。切片 2 是贯穿每一层的多租户作用域。切片 3 是只读 / 视图支撑的变体。切片 4 是定制覆盖。切片 5 是覆盖不够用时的逃生口。它们合起来从中心到边界覆盖数据驱动设计。