Skip to the content.

数据库影响判断:如何减少误报

跨程序数据库影响的判定目标不是「找出 A 和 B 是否访问同一张表」,而是:

A 的这次改动,是否改变了 B 能观察到的数据库契约或数据行为?

「同名表」只是候选,不是结论。本文说明判定层在候选之后还要经过哪些准入条件,以及每条规则为什么能减少误报、又会在哪里留下盲区。

1. 四个准入条件

一条数据库候选要成为跨程序影响,必须同时满足:

条件 问题 不满足时
同一数据库对象 两个程序真的能看见同一张表吗? 排除,并计数
变更可被观察 对方的读写方式能感知这次改动吗? 不命中;必要时记盲区
契约不兼容 改动会让对方已有的值、绑定或映射失效吗? 兼容变更只计数,不报告
解析足够确定 列名、类型、可空性是否真的解析出来了? 降低置信度或记盲区,不猜测

这四条分别对应下面四节。核心原则是:发现依赖不等于证明缺陷;无法证明时,宁可标成盲区,也不要伪装成命中或干净结果。

2. 同一数据库对象:表名不是身份

orders、user、order_item 这类表名几乎每个库都有。只按 table.column 匹配,会把无关程序拉进审核队列。

系统从两个来源读取数据库身份:

  1. 配置中的 JDBC URL:spring.datasource.url、spring.datasource.*.url、spring.datasource.hikari.jdbc-url 等,解析出数据库名。
  2. SQL 中的显式 schema:order_db.orders 的 order_db,或 DDL 中的 schema.table。

判定规则:

为什么只比较数据库名、不比较 host:同一套 schema 在开发、测试、生产通常通过不同 host 访问;host 不同不代表库不同。

嵌入式数据库(H2 mem、HSQLDB、Derby、SQLite)不参与身份比较:它们是进程内数据库,两个程序都把库命名为 testdb 并不共享任何数据。

3. 变更可被观察:不是所有 DDL 都会影响对方

数据库契约的单位是 table.column,但不同 DDL 对使用方的影响方式不同:

本程序改动 对方读方 对方写方 对方实体映射 判定
显式 DROP COLUMN(迁移脚本) 命中 命中 命中 列不存在,读写与映射都会失败
快照推断字段消失(无 DROP COLUMN) 不命中 不命中 不命中 仅改 CREATE TABLE 不会删除已部署列;只计数
RENAME COLUMN 命中(旧列) 命中(旧列) 命中(旧列) 旧列名消失;新列名不可能已是依赖
RENAME TABLE 命中(旧表) 命中(旧表) 命中(旧表) 旧表名消失,退回表级契约;新表名不可能已是依赖
MODIFY 收窄 / 类型不兼容 命中 命中 命中 值可能截断、精度丢失或隐式转换
MODIFY 放宽 / 兼容 不命中 不命中 不命中 已有值与绑定仍然有效,只计数
MODIFY 可空性收窄(NULL → NOT NULL) 不命中 命中 不命中 读方仍读到相同值;写方可能被拒绝
MODIFY 默认值变化 不命中 不命中 不命中 只影响省略该列的写入,进入盲区
ADD COLUMN 不命中 不命中 不命中 按列名读写的代码看不见新列
ADD COLUMN 且 NOT NULL 无默认值 不命中 命中(完整 INSERT 省略该列) 不命中 数据库拒绝省略该列的 INSERT;自动填充列除外
CREATE TABLE 不命中 不命中 不命中 新表不可能已经是对方的依赖
DROP TABLE 命中 命中 命中 整表消失,退回表级契约
删除迁移脚本 不命中 不命中 不命中 已执行的 migration 不会因为删文件而回滚
写入字段集合变化(新增写入列) 命中 不命中 不命中 本程序开始写某列,读方能观察到值变化
写入字段集合变化(停止写入列) 不命中 不命中 不命中 列仍然存在,读方 SQL 不会失败;只计数,不按字段删除传播
无列名的整表写入 命中 不命中 不命中 行级写入变化,但仅限解析不出列名时
实体字段新增 不命中 不命中 不命中 加映射是程序内部行为
实体字段删除 / Java 类型变化 不命中 不命中 低置信度提示 字段映射不是数据库 schema

表重命名:契约是旧表名

A: ALTER TABLE orders RENAME TO orders_archive;      -- 或 RENAME TABLE orders TO orders_archive;
B: SELECT id FROM orders WHERE id = ?

重命名之后旧表名在数据库里不存在了,B 读、写、映射 orders 的代码都会在部署当天失败。因此它按 表级契约命中,契约写旧表名 orders,置信度约 0.90;对方对旧表的所有使用点都受影响。

新表名 orders_archive 刻意不产生任何候选:它是这次改名才出现的,不可能已经是对方的依赖。 把它也当成契约,等于把同一次改名报两遍,还会凭空牵连碰巧叫这个名字的程序。

一条 ALTER 里的多个动作:按动作报告,不按语句

ALTER TABLE orders ADD a INT, DROP b, MODIFY c BIGINT NOT NULL, RENAME d TO e 是四条契约变更, 不是一条。只读第一个动作会漏报后三个;混合动作还曾把 ADD 的类型串到 DROP 的记录上。

新增列为什么不再命中读方

A: ALTER TABLE orders ADD COLUMN remark VARCHAR(64);
B: SELECT id, status FROM orders WHERE id = ?

B 的列清单是写死的,新列不会让 B 的 SQL 失败,也不会改变 B 读到的 id/status。真正可能受影响的是:

前两种无法用「对方是否读这一列」判断,因此进入字段盲区计数,而不是把每个读方都报成受影响。 第三种是确定的写侧不兼容,按下一节的规则命中写方。

NOT NULL 且无默认值:写方会失败,不再只是盲区

A: ALTER TABLE orders ADD COLUMN tenant VARCHAR(32) NOT NULL;   -- 无默认值
B: INSERT INTO orders (id, status) VALUES (?, ?);               -- 省略 tenant

MySQL 严格模式、PostgreSQL、Oracle 都会拒绝 B 的这条 INSERT。这是真实的不兼容,不是「无法判断」, 所以它命中写方:契约仍是 orders.tenant,usage=writer,置信度约 0.85。

判定同时要求:

以下情况不算必填列,因为数据库会自己补值:

有默认值的加列仍然只计数(「对对方不可观察」);SELECT * 与动态 SQL 不命中必填列, 仍计入字段盲区。

兼容变更为什么只计数

VARCHAR(32) → VARCHAR(64)     放宽,已有值全部可表示
INT → BIGINT                  放宽,已有值全部可表示
DECIMAL(10,2) → DECIMAL(12,2) 精度增加
DATE → DATETIME               时间信息增加
可空 → NOT NULL               收窄(写方可能失败)

类型兼容规则集中在 ColumnTypeCompatibility。只有收窄、类型族变化或无法判断时才命中;放宽只计入「兼容变更」计数,报告会说明有多少条候选因此没有扩散。

4. 解析足够确定:动态 SQL 不能降级成表级契约

字段级匹配依赖从 SQL 中解析出列名。以下情况解析结果只是下界,不是事实:

处理方式:

此外,SQL 符号按文件与行号标识,语句从一个 mapper 移到另一个会同时产生「删除」和「新增」。变更检测会按规范化文本配对这两条记录并取消;文件搬家不应被读成写入字段集合变化。

SELECT * 不是「没有使用这个字段」,而是「无法静态判断使用了哪些字段」。把盲区读成「没影响」是最容易犯的错误,所以它出现在报告里,而不是被静默丢弃。

5. 实体映射与数据库 schema 解耦

JPA 实体字段变化过去被当作 schema 变化,导致每次实体重构都命中对方读写方。现在:

真正的 schema 变化仍由 DDL / 迁移脚本捕获,与实体映射分开。

6. 报告如何保持透明

减少误报不能变成「悄悄不报」。报告会保留:

这些计数不参与风险分,但它们决定了「没有发现影响」到底有多可信。

7. 仍然没有解决的问题

问题 现状 下一步
写入列集合不变、值语义变了 不产生 delta 比较 SET/INSERT 赋值表达式;只命中按该列过滤的对方代码
事务边界、提交时机、锁顺序 不分析 解析 @Transactional、FOR UPDATE、更新顺序,结合并发测试
视图 / 触发器 / 存储过程 / 外键级联 不建依赖图 采集数据库元数据,沿对象依赖传播
真实数据分布 不读取 在隔离测试库执行迁移并校验数据
运行时实际 SQL 不采集 MyBatis 插件 / JDBC 代理 / OpenTelemetry 补全
多版本共存 静态契约兼容 扩展—迁移—收缩,校验新 A 与旧 B
同一程序多数据源 / 多 schema 同名表 快照键只有 table.column,可能互相覆盖 按数据源 / 模块隔离键
数据库自动填充列的识别 只看 ALTER 列定义文本里的 AUTO_INCREMENT / GENERATED / IDENTITY;触发器、序列默认值等其他自动生成机制未识别,可能仍按必填列处理 采集数据库元数据判断列是否自动生成

这些是已知边界,不是「已经支持」。报告中的「未发现影响」只表示在静态契约层没有找到不兼容证据。

8. 一张判定流程图

A 的代码 / SQL / DDL 变更
        │
        ▼
还原数据库 delta:动作级(表.列 / 表 + 操作 + 旧/新类型 + 可空性 + 默认值)
        │
        ├─ 重命名表 ──→ 旧表名按表级契约命中读写与映射;新表名不扩散
        │
        ├─ 删除迁移脚本 / 新增表 / 加列 / 兼容放宽 ──→ 不扩散(计数)
        │
        ├─ 必填列(NOT NULL 无默认值且非自动填充)──→ 命中省略该列的 INSERT 写方
        │
        ▼
查对方使用点:READS_COLUMN / WRITES_COLUMN / MAPS_COLUMN(表级:READS_TABLE / WRITES_TABLE / USES_TABLE)
        │
        ├─ 数据库身份不同 ──→ 排除(计数)
        │
        ├─ 操作类型不接受该使用点 ──→ 不命中
        │
        ▼
命中:进入对方调用图传播,计算风险与置信度
        │
        ▼
未命中但表相同且列名不可解析 ──→ 字段盲区(人工确认)

9. 相关文档