在现有Django应用中更改主键的最佳方法是什么?

58 投票
10 回答
79622 浏览
提问于 2025-04-15 17:58

我有一个正在测试中的应用程序。这个应用的模型里有一些类是明确指定了主键的。因此,Django就会使用这些字段,而不会自动创建一个id。

class Something(models.Model):
    name = models.CharField(max_length=64, primary_key=True)

我觉得这样做不太好(可以参考这个链接:在Django管理后台保存对象时出现unicode错误),我想改回去,让每个模型类都有一个id。

class Something(models.Model):
    name = models.CharField(max_length=64, db_index=True)

我已经对我的模型进行了修改,把每个primary_key=True的地方改成了db_index=True,并且我想用south来迁移数据库。

可惜的是,迁移时出现了以下错误信息: ValueError: 你不能添加一个null=False的列而没有默认值。

我正在考虑解决这个问题的不同方法。有没有什么建议?

谢谢你的帮助

10 个回答

10

我在使用django 1.10.4的迁移功能和mysql 5.5时成功做到了这一点,但过程并不简单。

我有一个用作主键的varchar字段,并且有几个外键。我添加了一个id字段,迁移了数据和外键。具体步骤如下:

  1. 添加未来的主键字段。我在我的主模型中添加了一个id = models.IntegerField(default=0)字段,并生成了一个自动迁移。
  2. 简单的数据迁移以生成新的主键:

    def fill_ids(apps, schema_editor):
       Model = apps.get_model('<module>', '<model>')
       for id, code in enumerate(Model.objects.all()):
           code.id = id + 1
           code.save()
    
    class Migration(migrations.Migration):
        dependencies = […]
        operations = [migrations.RunPython(fill_ids)]
    
  3. 迁移现有的外键。我写了一个组合迁移:

    def change_model_fks(apps, schema_editor):
        Model = apps.get_model('<module>', '<model>')  # Our model we want to change primary key for
        FkModel = apps.get_model('<module>', '<fk_model>')  # Other model that references first one via foreign key
    
        mapping = {}
        for model in Model.objects.all():
            mapping[model.old_pk_field] = model.id  # map old primary keys to new
    
        for fk_model in FkModel.objects.all():
            if fk_model.model_id:
                fk_model.model_id = mapping[fk_model.model_id]  # change the reference
                fk_model.save()
    
    class Migration(migrations.Migration):
        dependencies = […]
        operations = [
            # drop foreign key constraint
            migrations.AlterField(
                model_name='<FkModel>',
                name='model',
                field=models.ForeignKey('<Model>', blank=True, null=True, db_constraint=False)
            ),
    
            # change references
            migrations.RunPython(change_model_fks),
    
            # change field from varchar to integer, drop index
            migrations.AlterField(
                model_name='<FkModel>',
                name='model',
                field=models.IntegerField('<Model>', blank=True, null=True)
            ),
        ]
    
  4. 交换主键并恢复外键。再次使用自定义迁移。当我 a) 从旧主键中移除primary_key=True,以及 b) 移除id字段时,我自动生成了这个迁移的基础。

    class Migration(migrations.Migration):
        dependencies = […]
        operations = [
            # Drop old primary key
            migrations.AlterField(
                model_name='<Model>',
                name='<old_pk_field>',
                field=models.CharField(max_length=100),
            ),
    
            # Create new primary key
            migrations.RunSQL(
                ['ALTER TABLE <table> CHANGE id id INT (11) NOT NULL PRIMARY KEY AUTO_INCREMENT'],
                ['ALTER TABLE <table> CHANGE id id INT (11) NULL',
                 'ALTER TABLE <table> DROP PRIMARY KEY'],
                state_operations=[migrations.AlterField(
                    model_name='<Model>',
                    name='id',
                    field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
                )]
            ),
    
            # Recreate foreign key constraints
            migrations.AlterField(
                model_name='<FkModel>',
                name='model',
                field=models.ForeignKey(blank=True, null=True, to='<module>.<Model>'),
        ]
    
11

如果你想用south来更改主键,可以在数据迁移中使用south.db.create_primary_key命令。要把你自定义的CharField主键改成标准的AutoField,你需要按照以下步骤操作:

1) 在你的模型中创建一个新的字段

class MyModel(Model):
    id = models.AutoField(null=True)

1.1) 如果在其他模型中有外键指向这个模型,也要在这些模型中创建一个假的外键字段(用IntegerField,这样之后会被转换)

class MyRelatedModel(Model):
    fake_fk = models.IntegerField(null=True)

2) 创建一个自动的south迁移并进行迁移:

./manage.py schemamigration --auto
./manage.py migrate

3) 创建一个新的数据迁移

./manage.py datamigration <your_appname> fill_id

在这个数据迁移中,用数字填充这些新的id和外键字段(就是给它们编号)

    for n, obj in enumerate(orm.MyModel.objects.all()):
        obj.id = n
        # update objects with foreign keys
        obj.myrelatedmodel_set.all().update(fake_fk = n)
        obj.save()

    db.delete_primary_key('my_app_mymodel')
    db.create_primary_key('my_app_mymodel', ['id'])

4) 在你的模型中,把新的主键字段设置为primary_key=True

id = models.AutoField(primary_key=True)

5) 删除旧的主键字段(如果不需要的话),创建自动迁移并进行迁移。

5.1) 如果有外键的话,也要删除旧的外键字段(然后迁移)

6) 最后一步 - 恢复外键关系。再次创建真实的外键字段,删除你的假外键字段,创建自动迁移,但不要进行迁移!你需要修改刚创建的自动迁移:把创建新的外键和删除假外键的操作改成重命名假外键字段。

# in your models
class MyRelatedModel(Model):
    # delete fake_fk
    # fake_fk = models.InegerField(null=True)
    # create real fk
    mymodel = models.FoeignKey('MyModel', null=True)

# in migration
    def forwards(self, orm):
        # left this without change - create fk field
        db.add_column('my_app_myrelatedmodel', 'mymodel',
                  self.gf('django.db.models.fields.related.ForeignKey')(default=1, related_name='lots', to=orm['my_app.MyModel']),keep_default=False)

        # remove fk column and rename fake_fk
        db.delete_column('my_app_myrelatedmodel', 'mymodel_id')
        db.rename_column('my_app_myrelatedmodel', 'fake_fk', 'mymodel_id')

这样之前填充的假外键就变成了一个包含实际关系数据的列,确保在以上所有步骤后这些数据不会丢失。

105

我同意,你的模型可能是错的。

正式的主键应该始终是一个替代键,绝不要用其他的东西。 [这话说得很重。我从1980年代开始做数据库设计,学到的重要教训就是:所有东西都是可以改变的,即使用户发誓说某个值不能变,它是一个自然键,可以作为主键,但其实它并不是主键。只有替代键才能是主键。]

你现在是在做心脏手术,别去乱动架构迁移。你是在替换架构。

  1. 把你的数据导出到JSON文件中。可以使用Django自带的django-admin.py工具来完成这项工作。你应该为每一个要改变的表和每一个依赖于正在创建的键的表创建一个导出文件。分开文件会让这个过程稍微简单一些。

  2. 删除你要从旧架构中更改的表。

    依赖于这些表的其他表的外键(FK)也会被更改;你可以选择直接更新这些行,或者——可能更简单——删除并重新插入这些行。

  3. 创建新的架构。这只会创建那些正在改变的表。

  4. 写脚本来读取并用新的键重新加载数据。这些脚本很短且非常相似。每个脚本会使用 json.load() 从源文件中读取对象;然后你会根据为你构建的JSON元组对象创建架构对象。接着,你可以将它们插入到数据库中。

    你有两种情况。

    • 主键(PK)发生变化的表会被插入并获得新的主键。这些主键必须“级联”到其他表,以确保其他表的外键也被更改。

    • 外键(FK)发生变化的表需要找到外部表中的行并更新它们的外键引用。

另一种方法。

  1. 重命名你所有的旧表。

  2. 创建整个新的架构。

  3. 写SQL语句将所有数据从旧架构迁移到新架构。这需要巧妙地重新分配键。

  4. 删除重命名后的旧表。

 

撰写回答