# 无物理外键、语义外键的现实

`xlyweberp_saas_ai` schema 在任何 xly 自研表（`gds*`、`ele*`、`mft*`、`quo*`、`sal*`、`acc*` 等表族）上都有 **0 个触发器**和 **0 个外键约束**。确实存在的 FK 约束都在捆绑的第三方 schema 上：Activiti 的 `act_*` 表和 Quartz 的 `qrtz_*` 表；框架自身运行时不会通过这些表做核心连接。框架依赖的元数据和业务数据连接全部只是约定。

这是一个有意的设计选择，继续阅读前必须理解。

## 为什么 xly 禁用 FK

架构上给出的两个原因是：

1. **批量写入性能。** 大量插入（工单计算、月结、批量导入）一次会写入几十万行。启用 FK 后，MySQL 会在插入时验证每一行引用；以 xly 的数据量，这会成为限制因素。
2. **schema 迁移敏捷性。** xly 演进很快：新模块、新字段、新表。启用 FK 时，每次 schema 变更都必须考虑约束图；没有 FK 时，`CREATE TABLE` 或 `ALTER TABLE` 是局部操作。

这两个考虑都真实存在，但都不足以推出“整个 schema 零 FK”：

- **批量写入性能**可以更精细地处理：批处理期间临时关闭约束（`SET FOREIGN_KEY_CHECKS = 0`），写完后重新打开并校验。xly 选择的是完全不建 FK，这意味着每次读取也都要相信临时过程校验，而不是数据库强制的完整性。
- **schema 迁移敏捷性**确实会因为没有 FK 而提高，但代价是每个引用检查都要挪到应用代码或存储过程里，甚至可能被遗漏。实践中，原本 FK 自动完成的完整性工作被复制到数百个存储过程中，而且没有编译期保证某个过程真的做了检查（见下面的失效模式）。

更准确的说法是：系统用 **DB 强制完整性**换取了**写入时和 DDL 时的运维便利**。这笔交易带来的 bug 面（孤儿行、未发现的跨租户引用、几周后才浮现的完整性错误）会在系统每天运行时持续付费。

## 什么是“语义 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、应该信什么”的规范参考。
