没有 FOREIGN KEY 的库,如何让 ER 图连上线:tbls 与 SchemaSpy 的关系推断
老库、分库分表之后的库,以及为了写入性能刻意不建 FOREIGN KEY 的库,表之间往往只剩下命名约定:order.user_id、log.post_id。约束层的 FOREIGN KEY 不存在,用 tbls 或 SchemaSpy 生成文档时,ER 图就是一堆孤岛,每张表单独站着,没有一条线。
要补线,只有两条路:在文档工具里显式声明关系,或者让它按列名去猜,再把猜出来的结果固定下来。
tbls 的两条补关系路径
项目地址:k1LoW/tbls(Go 写的单二进制文档工具,适合进 CI)。
手动声明 relations
README 原文:relations: is used to add or override table relation to database document without FOREIGN KEY. You can create ER diagrams with relations without having foreign key constraints.
relations:
-
table: logs
columns:
- user_id
parentTable: users
parentColumns:
- id
# Relation definition
# Default is `Additional Relation`
def: logs->users
-
table: logs
columns:
- post_id
parentTable: posts
parentColumns:
- id
-
table: logs
columns:
- comment_id
parentTable: comments
parentColumns:
- id
这部分不动数据库,只写进 .tbls.yml 随仓库走。
自动推断 detectVirtualRelations
detectVirtualRelations:
enabled: true
strategy: default
README 表格给出的五种策略(relation from → relation to):
| strategy | 匹配形式 |
|---|---|
default | some_table.user_id → users.id |
singularTableName | some_table.user_id → user.id |
identical | some_table.user_id → users.user_id |
identicalSingularTableName | some_table.user_id → user.user_id |
invertedSingularTableName | some_table.id_user → user.id |
default 假定表名是复数(users)、父表主键是 id;singularTableName 假定表名是单数(user);identical 系列假定父子列同名,不要求父列是主键 id,适合 user_id 本身就是父表主键的情况;invertedSingularTableName 只认 id_user 这种反写形式。
误判与边界
推断的每一处都带有猜测成分,常见的边界有:
- 同名约定在多个表里重复:
logs.user_id可能指向users,也可能指向operator_users、admin_users。策略只会命中一张,其余得手写relations。 - 父表主键不是
id(比如uuid、user_no):default全部落空,应换identical系策略,或在配置里改父表列。 - 一张表里
user_id出现在多个语义位置:from_user_id、to_user_id、operator_id。默认策略只对user_id这种字面量生效,语义列要显式写relations。 - 同一对表之间存在两条关系时,用
def区分,例如logs->users与logs->operators。 detectVirtualRelations只能补「看不出来的线」,不能纠正错误基数;基数要靠relations+cardinality/parentCardinality+override。
更正基数:override 与 cardinality
库里确实有 FK、但约束或推断给出的基数不对(例如实际是 0..1 对 1..n),用 override: true 覆盖:
relations:
-
table: posts
columns:
- user_id
cardinality: zero or one
parentTable: users
parentColumns:
- id
parentCardinality: one or more
override: true
def: posts->users
cardinality 与 parentCardinality 分别设本端与父端的取值,override: true 表示替换已存在的关系而不是追加一条。
SchemaSpy:implied relationships 与 SchemaMeta
项目地址:schemaspy/schemaspy,官网 schemaspy.org。
SchemaSpy 除真实外键外,会按列名、类型、大小推断隐式关系(implied relationships),这同样是猜。文档给出的矫正手段是 SchemaMeta XML,能做几件事:
- 添加 comments/remarks(
remarks已废弃,统一用comments,支持 markdown) - 添加 relationships
- 添加 remote tables(
remoteCatalog/remoteSchema,逻辑表) - 添加 columns
- 把列从 implied relationships 中排除:Available options:
to,from,all,none;Default:none
给不存在外键的列硬指定父表列:
反过来,避免 acId 被自动连到 agents 之类的表,就用 exclude 的 implied 选项,取值 to/from/all/none。
验证:让图说话
补完线,得看图确认连没连对。tbls 支持多种输出格式:
$ tbls out -t mermaid -o schema.mmd
$ tbls out -t dot -o schema.dot
$ tbls out -t svg --table logs --distance 2 -o logs.svg
--table 配 --distance 只画某张表周围 N 层,比全库大图可读得多。tbls out -t md --viewpoint 输出某个视角的文档(含 group 与内联 mermaid ER 图),视角在 .tbls.yml 的 viewpoints 里定义。
想离线或无库复现,用 tbls doc json:///path/to/schema.json,其中 schema.json 由 tbls out -t json -o schema.json 生成;doc 会把 schema.json 一起写进文档目录,disableOutputSchema: true 可关掉。
覆盖率用 tbls coverage,报表和列的描述覆盖率。缺注释的表往往就是关系没补全的表。
取舍
- 只是让新人看懂库:先把关键链路(订单—支付—物流)用
relations手写十几条,比全库自动推断可靠;自动推断只做兜底。 - 文档要进 CI 做漂移检查:推断结果必须固定下来——检出
detectVirtualRelations的结果后转成显式relations,否则升级版本或改命名会让图无声变动。 - 真要长期准确:加真实
FOREIGN KEY(新表上成本低),或至少把关系定义放进仓库与代码同版本管理。 - SchemaSpy 适合发布静态 HTML 站点:Java 17+ 或 Docker 镜像;JAR 发行版不带 JDBC 驱动,Docker 镜像自带 MySQL/MariaDB/PostgreSQL/jTDS 驱动。
- 走 DBML 路线:dbml.dbdiagram.io/cli(
@dbml/cli,sql2dbml/db2dbml生成 DBML),仓库 holistics/dbml,其中的Ref行就是显式关系。
各参数的具体默认值,需按自己版本的 -help 验证。