编程 没有 FOREIGN KEY 的库,如何让 ER 图连上线:tbls 与 SchemaSpy 的关系推断

2026-11-11 00:04:10

没有 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匹配形式
defaultsome_table.user_id → users.id
singularTableNamesome_table.user_id → user.id
identicalsome_table.user_id → users.user_id
identicalSingularTableNamesome_table.user_id → user.user_id
invertedSingularTableNamesome_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 验证。

推荐文章

程序员茄子在线接单