# 无物理外键、语义外键的现实 `xlyweberp_saas_ai` schema 在任何 xly 自研表(`gds*`、`ele*`、`mft*`、`quo*`、`sal*`、`acc*` 等表族)上都有 **0 个触发器**和 **0 个外键约束**。确实存在的 41 个 FK 约束都在捆绑的第三方 schema 上:Activiti 表(`act_*`)36 个、Quartz 调度表(`qrtz_*`)5 个;框架自身运行时不会通过这些表做核心连接。在 901 张基础表中,框架依赖的元数据和业务数据连接全部只是约定。 这是一个有意的设计选择,继续阅读前必须理解。 ## 为什么 xly 禁用 FK 架构上给出的两个原因都很务实: 1. **批量写入性能。** 大量插入(工单计算、月结、批量导入)一次会写入几十万行。启用 FK 后,MySQL 会在插入时验证每一行引用;以 xly 的数据量,这会成为限制因素。 2. **schema 迁移敏捷性。** xly 演进很快:新模块、新字段、新表。启用 FK 时,每次 schema 变更都必须考虑约束图;没有 FK 时,`CREATE TABLE` 或 `ALTER TABLE` 是局部操作。代价由运行时应用代码承担。 ## 什么是“语义 FK” **语义 FK** 指如果启用 FK 本该成为外键、但实际上没有约束的列。关系编码在: - **列命名。** `sCustomerId` 被理解为引用 `eleCustomer.sId`。从表上的 `sParentId` 被理解为引用主表的 `sId`。 - **存储过程和 MyBatis mapper 中的共同出现。** 运行时在很多地方连接 `A.sParentId = B.sId`;阅读过程体能发现哪些表配对。 - **列注释。** 许多列填了 `COLUMN_COMMENT`,并常常指出引用概念,例如 `加工商ID`。 - **元数据声明。** 框架的 `gdsconfigformslave.sActiveId` / `sActiveKey` 会明确写出下拉控件指向的*表单*和*字段*,这是一种元数据编码的 FK。 这些都不会被数据库强制。你可以从 `eleCustomer` 删除一行,数据库不会阻止;孤儿 `sCustomerId` 引用会继续存在。 ## 恢复关系 当自动目录或 Wiki 需要知道“列 X 引用什么”时,查找流程是: 1. 读列注释,通常这是最有帮助的一行。 2. 在 schema 中搜索匹配的 ID 列(在 `recon/columns.tsv` 中 `grep` `XSomethingId`)。 3. 在代码库中搜索在该列上 JOIN 的 SQL 片段。 4. 检查 `gdsconfigformslave.sActiveId`,框架会在这里记录下拉和 lookup。 5. 最后再按主从命名约定推断。 **自动目录**的目标就是简化这些工作:每个生成的表页列出字段,并在未来增强中列出引用它们的过程,从而缩小搜索范围。 ## 失效模式 禁用 FK 的成本在运行时体现。三种失效模式反复出现: - **孤儿行。** 比如客户被删除后仍存在的 `sCustomerId`。框架查询通过 left join 穿过这些行,可能静默丢弃,也可能显示空白。 - **跨租户 ID 不匹配。** 没有 FK 时,没有东西阻止一行引用另一个 `sBrandsId` 下的 ID。多租户过滤(切片 2)是阻止它演变成跨租户泄漏的唯一边界。 - **需要手动验证引用完整性的存储过程。** 许多 xly 存储过程在插入前显式检查 `EXISTS (...)`。本该由 FK 自动完成的完整性工作散布在过程体中。漏掉一次,bug 可能几周后才暴露。 本 Wiki 的任务是在数据库不强制的情况下暴露这些关系。大部分暴露发生在自动目录和切片交叉引用中;本页是“为什么没有 FK、应该信什么”的规范参考。