DEBUG.WIKI
雨云;给项目一台云服务器
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →
PanstarCloud:部署应用,接入 AI,云服务器与 AI 网关

SQLAlchemy 复制表后外键丢了 NOT VALID,检查 to_metadata 版本

旧 Table.to_metadata 可能丢失 ForeignKeyConstraint 的方言选项。正式 2.1.5 与 2.0.55 已修正;同时比较复制前后约束和生成 DDL,迁移稿不能只看表是否存在。

原表外键声明了 postgresql_not_valid=True,调用 Table.to_metadata() 复制到新 MetaData 后,生成的 DDL 却不再含 NOT VALID。迁移脚本里的表名、列和引用都看起来正确,也不能排除约束语义在复制时丢失。

旧的 ForeignKeyConstraint 复制路径没有像其他某些约束一样保留方言专用参数。列上简写的 ForeignKey(...) 也可能归属于外键约束并经由这条路径复制,因此只更换声明位置不一定有效。明确的环境包括 SQLAlchemy 2.0.52 与 2.1.3。

使用包含修正的正式版本

正式 2.1.5、2.0.55 的发行说明列出了这项修复。当前 2.1.6 的 ForeignKeyConstraint._copy() 也会把 dialect_kwargs 交给新约束。按项目所在系列采用兼容版本后,仍要检查原有迁移生成入口,而不只检查一个新建的简单 Table。

下面只构造元数据、编译 DDL,不执行数据库变更:

from sqlalchemy import (
    Column, ForeignKeyConstraint, Integer, MetaData, Table,
)
from sqlalchemy.dialects import postgresql
from sqlalchemy.schema import AddConstraint

original = MetaData()
parent = Table("parent", original, Column("id", Integer, primary_key=True))
child = Table(
    "child", original, Column("parent_id", Integer),
    ForeignKeyConstraint(
        ["parent_id"], ["parent.id"], name="fk_parent",
        postgresql_not_valid=True,
    ),
)
copied = MetaData()
parent.to_metadata(copied)
child_copy = child.to_metadata(copied)
fk = next(c for c in child_copy.constraints if c.name == "fk_parent")
print(dict(fk.dialect_kwargs))
print(AddConstraint(fk).compile(dialect=postgresql.dialect()))

预期复制后的选项仍含 postgresql_not_valid=True,编译结果保留 NOT VALID。复制目标引用的表也在示例中先加入新 MetaData,避免把引用缺失造成的另一个编译错误混进来。

重新检查已经生成的迁移稿

升级库不会自动改好早先保存的 Alembic 文件。若那份文件已丢失外键选项,应对照原始元数据和批准的数据库约束设计重新审阅;不能把所有外键批量加上 NOT VALID,更不能为了绕开已有数据问题删除约束。

NOT VALID 的用途和后续验证安排属于实际数据库变更流程,保留原有备份、锁与验证要求。对于其他方言选项,也要按对应方言核对,不能从 PostgreSQL 例子推断每个驱动都支持同名参数。

复核至少覆盖表级和列级外键、不同 schema 的引用,以及应用真正使用的复制和迁移生成流程。成功条件是复制后的约束与预期 DDL 一致,而不是仅仅没有抛异常。