FastAPI/SQLAlchemy中Alembic迁移重复检测id唯一约束的原因
我在FastAPI/SQLAlchemy项目中使用Alembic做数据库迁移时遇到了异常:
初始迁移执行正常,执行的命令及日志如下:
alembic revision --autogenerate -m "InitialMigration" ---Connecting to PostgreSQL--- INFO [alembic.runtime.migration] Context impl PostgresqlImpl. INFO [alembic.runtime.migration] Will assume transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'tasks' INFO [alembic.autogenerate.compare] Detected added table 'solutions' INFO [alembic.autogenerate.compare] Detected added table 'tasks_files' Generating /usr/src/app/app/alembic/versions/05f3f7393aa7_initialmigration.py ... done alembic upgrade head ---Connecting to PostgreSQL--- INFO [alembic.runtime.migration] Context impl PostgresqlImpl. INFO [alembic.runtime.migration] Will assume transactional DDL. INFO [alembic.runtime.migration] Running upgrade -> 05f3f7393aa7, InitialMigration
初始迁移文件中正确创建了tasks表,同时定义了主键约束与id字段的唯一约束:
def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('tasks', sa.Column('id', sa.String(), nullable=False), sa.Column('external_id', sa.String(), nullable=False), ... sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('id') )
之后我给Task模型添加了new_field字段:
class Task(Base): __tablename__ = 'tasks' id = Column(String, unique=True, nullable=False, primary_key=True) external_id = Column(String, nullable=False) ... new_field = Column(String)
再次执行自动生成迁移命令时,日志反复出现:
INFO [alembic.autogenerate.compare] Detected added unique constraint None on '('id',)'
生成的迁移文件里还包含了多余的、针对各表id字段的唯一约束创建语句:
def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_unique_constraint(None, 'solutions', ['id']) op.add_column('tasks', sa.Column('new_field', sa.String(), nullable=True)) op.create_unique_constraint(None, 'tasks', ['id']) op.create_unique_constraint(None, 'tasks_files', ['id']) # ### end Alembic commands ###
请问为什么会出现这种重复检测id字段唯一约束的情况?
核心原因
主键自带唯一约束:SQL标准里,主键字段本身就隐含了唯一约束,不需要额外显式声明
unique=True。当你在模型的主键字段上同时设置primary_key=True和unique=True时,SQLAlchemy会同时生成主键约束和唯一约束;而Alembic对比数据库状态和模型定义时,数据库中主键的唯一约束是隐式维护的,无法和模型中显式的unique=True对应上,导致每次自动迁移都误判需要新增唯一约束。初始迁移的冗余定义:第一次自动生成迁移时,Alembic根据你的模型生成了同时包含主键约束和唯一约束的表创建语句,这本身就是冗余的。后续迁移时,Alembic对比数据库(此时主键的唯一约束是隐式的)和模型(显式声明
unique=True),就会重复触发唯一约束的检测。
解决步骤
移除模型中主键字段的
unique=True:直接删除主键定义里的unique=True,修改为:id = Column(String, nullable=False, primary_key=True)清理现有迁移文件中的冗余约束:
- 对已生成的迁移文件,删除其中多余的
sa.UniqueConstraint('id')和op.create_unique_constraint(None, 'xxx', ['id'])语句; - 如果已经执行了包含冗余约束的迁移,可以生成新迁移来删除这些多余的唯一约束,或者手动在数据库中删除(操作前请备份数据)。
- 对已生成的迁移文件,删除其中多余的
验证后续迁移:修改模型后重新执行
alembic revision --autogenerate,此时应该只会生成和实际变更相关的迁移代码,不会再出现重复创建id字段唯一约束的情况。
内容的提问来源于stack exchange,提问作者mascai

