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