# 数据驱动的基本论点

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/客户/<customer>/`，并直接应用到该客户 schema，完全绕过框架。

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

## 这对阅读 Wiki 意味着什么

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