基于Alembic的MSSQL允许空值唯一约束列配置与降级问题
MSSQL + Alembic 实现「列唯一约束 + 允许多空值」的正确方案
问题根源
- 插入两条空值触发2627报错:MSSQL的普通唯一约束会把NULL当成有确定值的可比较对象,只要约束键重复(哪怕都是NULL)就会抛冲突错误,这点和PostgreSQL默认允许多个NULL的逻辑完全不一样。
- 降级流程卡住:如果直接在Column定义里写
unique=True,Alembic会自动生成带随机哈希后缀的匿名唯一约束(类似UQ__TABLE__reference__7A3B8F...),删列的时候MSSQL不会自动级联删掉这个绑定的约束,而且这种随机名每个环境生成的结果都不一样,根本没法提前写死在迁移脚本里,自然没法顺利降级。
正确的迁移脚本写法
直接把唯一属性绑在Column上的写法从根上就有问题,需要拆成两步操作,用MSSQL原生的过滤索引实现需求:
升级阶段(upgrade)
def upgrade(): # 第一步:添加可空的reference列,不附带任何自动生成的唯一约束 op.add_column( 'TABLE', sa.Column('reference', sa.Integer(), nullable=True) ) # 第二步:创建固定命名的过滤唯一索引 op.create_index( 'UQ_TABLE_reference_non_null', # 手动指定统一名称,所有环境生效一致 table_name='TABLE', columns=['reference'], unique=True, # *核心配置*:只对非空值做唯一校验,跳过所有NULL值 mssql_where=sa.text('reference IS NOT NULL') )
这套逻辑完全匹配需求:非空的reference值严格保证全局唯一,想插多少个NULL都不会触发唯一冲突。
降级阶段(downgrade)
因为索引名是手动写死的固定值,不存在随机生成的情况,降级时按依赖顺序反向删除就行:
def downgrade(): # 第一步:先删掉绑定的唯一索引,解除列和索引的依赖关系 op.drop_index('UQ_TABLE_reference_non_null', table_name='TABLE') # 第二步:再正常删除列 op.drop_column('TABLE', 'reference')
为什么这套写法能同时解决两个问题
- 适配MSSQL的校验规则:过滤索引把NULL值排除在唯一校验范围外,从根源上避免了多NULL插入触发2627错误的问题,同时索引结构也不会存储无意义的NULL值,存储开销更小。
- 完全规避自动命名的坑:所有关联的索引都是手动指定的固定名称,没有Alembic自动生成的随机后缀,不管迁移在哪个环境、哪个版本的SQLAlchemy下执行,对象名称都统一,不需要临时去系统表查随机生成的约束名,降级流程完全可控。
内容的提问来源于stack exchange,提问作者Daniel Butler
相关产品推荐
相关产品推荐

