Django中将UUID主键迁移为Slug主键的生产环境实施方案
Django 主键从 UUID 迁移到 Slug 的多步骤生产安全方案
针对你的场景,要把AAA模型的主键从UUID切换为Slug,同时保证关联模型的引用正常,且生产环境无长时间阻塞,以下是分阶段的实操方案:
前提确认
- 当前AAA的
slug字段由AutoSlugField生成,依赖唯一的title字段,默认已保证唯一性; - 所有关联模型(如BBB)均通过UUID外键关联AAA;
- 操作前必须备份数据库,并在测试环境完成全流程验证。
步骤1:确保Slug字段满足主键要求
首先要明确slug字段具备主键的核心属性:非空、唯一。修改AAA模型:
class AAA(models.Model): id = UUIDField(primary_key=True, version=4, editable=False) title = models.CharField(max_length=255, unique=True) # 显式添加unique=True,确保Slug唯一(AutoSlugField默认可能已处理,但显式更安全) slug = AutoSlugField(populate_from='title', primary_key=False, unique=True)
生成并执行迁移:
python manage.py makemigrations python manage.py migrate
注意:这一步仅强化Slug的约束,不改动主键,对生产环境无影响。
步骤2:为AAA添加临时UUID字段(保留旧主键)
添加old_uuid字段存储原UUID主键,避免后续步骤丢失关联依据:
class AAA(models.Model): id = UUIDField(primary_key=True, version=4, editable=False) title = models.CharField(max_length=255, unique=True) slug = AutoSlugField(populate_from='title', primary_key=False, unique=True) # 添加临时字段,存储原UUID old_uuid = models.UUIDField(unique=True, editable=False, null=True)
生成迁移后,编辑迁移文件,添加RunPython操作批量赋值:
from django.db import migrations def copy_uuid_to_old_uuid(apps, schema_editor): AAA = apps.get_model('your_app', 'AAA') # 分批更新避免锁表(数据量大时建议按id分段处理) for obj in AAA.objects.all().iterator(): obj.old_uuid = obj.id obj.save(update_fields=['old_uuid']) class Migration(migrations.Migration): dependencies = [ ('your_app', 'previous_migration_name'), ] operations = [ migrations.AddField( model_name='aaa', name='old_uuid', field=models.UUIDField(editable=False, null=True, unique=True), ), migrations.RunPython(copy_uuid_to_old_uuid), ]
执行迁移:
python manage.py migrate
步骤3:为关联模型添加临时Slug外键字段
以BBB模型为例,添加aaa_slug字段,同步现有关联的Slug值:
class BBB(models.Model): aaa = models.ForeignKey(AAA, on_delete=models.CASCADE) # 添加临时字段,存储关联AAA的Slug aaa_slug = models.SlugField(null=True) # ... 其他字段 ...
生成迁移后,编辑迁移文件添加批量赋值逻辑:
from django.db import migrations def copy_aaa_slug_to_temp_field(apps, schema_editor): BBB = apps.get_model('your_app', 'BBB') # 分批处理,避免长时间锁表 for obj in BBB.objects.all().iterator(): obj.aaa_slug = obj.aaa.slug obj.save(update_fields=['aaa_slug']) class Migration(migrations.Migration): dependencies = [ ('your_app', 'previous_migration_name'), ] operations = [ migrations.AddField( model_name='bbb', name='aaa_slug', field=models.SlugField(null=True), ), migrations.RunPython(copy_aaa_slug_to_temp_field), ]
执行迁移:
python manage.py migrate
步骤4:切换关联模型的外键指向Slug
分小步修改,避免一次性锁表:
- 先允许原外键字段为空:
class BBB(models.Model): # 临时设置null=True,避免迁移时因数据关联报错 aaa = models.ForeignKey(AAA, on_delete=models.CASCADE, null=True) aaa_slug = models.SlugField(null=True) # ... 其他字段 ...
生成并执行迁移:
python manage.py makemigrations python manage.py migrate
- 修改外键指向AAA的Slug,并同步临时字段的值:
class BBB(models.Model): # 外键指向AAA的slug字段 aaa = models.ForeignKey(AAA, on_delete=models.CASCADE, null=True, to_field='slug') aaa_slug = models.SlugField(null=True) # ... 其他字段 ...
生成迁移后,编辑迁移文件添加同步逻辑:
from django.db import migrations def sync_temp_slug_to_foreign_key(apps, schema_editor): BBB = apps.get_model('your_app', 'BBB') AAA = apps.get_model('your_app', 'AAA') for obj in BBB.objects.all().iterator(): if obj.aaa_slug: obj.aaa = AAA.objects.get(slug=obj.aaa_slug) obj.save(update_fields=['aaa']) class Migration(migrations.Migration): dependencies = [ ('your_app', 'previous_migration_name'), ] operations = [ migrations.AlterField( model_name='bbb', name='aaa', field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='your_app.aaa', to_field='slug'), ), migrations.RunPython(sync_temp_slug_to_foreign_key), ]
执行迁移:
python manage.py migrate
- 恢复外键非空约束:
class BBB(models.Model): aaa = models.ForeignKey(AAA, on_delete=models.CASCADE, to_field='slug') aaa_slug = models.SlugField(null=True) # ... 其他字段 ...
生成并执行迁移:
python manage.py makemigrations python manage.py migrate
步骤5:切换AAA的主键为Slug
修改AAA模型,将slug设为主键,原UUID字段改为普通唯一字段:
class AAA(models.Model): # 原主键改为普通唯一字段,不再是primary_key uuid = models.UUIDField(unique=True, editable=False) title = models.CharField(max_length=255, unique=True) # 设置slug为主键 slug = AutoSlugField(populate_from='title', primary_key=True, unique=True) old_uuid = models.UUIDField(unique=True, editable=False, null=True)
生成迁移后,检查迁移文件,确保操作顺序是:先移除原id的主键属性,再设置slug为主键。如果Django生成的迁移有问题,手动调整操作顺序:
from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('your_app', 'previous_migration_name'), ] operations = [ # 先修改原id字段为普通字段,并重命名为uuid migrations.RenameField( model_name='aaa', old_name='id', new_name='uuid', ), migrations.AlterField( model_name='aaa', name='uuid', field=models.UUIDField(editable=False, unique=True), ), # 再设置slug为主键 migrations.AlterField( model_name='aaa', name='slug', field=django_extensions.db.fields.AutoSlugField(primary_key=True, populate_from='title', unique=True), ), ]
执行迁移:
python manage.py migrate
步骤6:清理临时字段
删除不再需要的临时字段:
class AAA(models.Model): uuid = models.UUIDField(unique=True, editable=False) title = models.CharField(max_length=255, unique=True) slug = AutoSlugField(populate_from='title', primary_key=True, unique=True) # 移除old_uuid字段 # old_uuid = models.UUIDField(unique=True, editable=False, null=True) class BBB(models.Model): aaa = models.ForeignKey(AAA, on_delete=models.CASCADE, to_field='slug') # 移除aaa_slug字段 # aaa_slug = models.SlugField(null=True) # ... 其他字段 ...
生成并执行迁移:
python manage.py makemigrations python manage.py migrate
生产环境注意事项
- 所有迁移步骤必须在业务低峰期执行,每个步骤单独执行,避免批量操作长时间锁表;
- 数据量较大时,
RunPython中的批量更新要改为分段处理(比如按id__range分批),避免内存溢出和锁表; - 执行每个迁移前,暂停可能写入AAA或关联模型的业务操作(如定时任务、API写入),迁移完成后恢复;
- 若使用读写分离,需确保迁移完成后同步从库数据。
内容的提问来源于stack exchange,提问作者wowkin2
相关产品推荐
相关产品推荐

