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

时间:2010-01-13 10:00:17

标签: python django data-migration django-south

我有一个处于BETA模式的应用程序。这个应用程序的模型有一些带有显式primary_key的类。因此,Django使用字段并且不会自动创建id。

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

我认为这是一个坏主意(请参阅unicode error when saving an object in django admin),我想回过头来为我的模特的每个班级都有一个id。

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

我对模型进行了更改(通过db_index = True替换每个primary_key = True),我想用south迁移数据库。

不幸的是,迁移失败并显示以下消息: ValueError: You cannot add a null=False column without a default value.

我正在评估此问题的不同解决方法。有什么建议吗?

感谢您的帮助

9 个答案:

答案 0 :(得分:60)

同意,你的模型可能不对。

正式主键应始终是代理键。从来没有别的。 [强烈的话语。自1980年代以来一直是数据库设计师。重要的经验教训就是:一切都是可以改变的,即使用户在母亲的坟墓上发誓价值无法改变,这确实是一个可以作为主要的自然关键。这不是主要的。只有代理才能成为主要代理人。]

你正在进行心内直视手术。不要乱用模式迁移。您正在替换架构。​​

  1. 将数据卸载到JSON文件中。使用Django自己的内部django-admin.py工具。您应该为每个将要更改的文件和每个依赖于正在创建的密钥的表创建一个卸载文件。单独的文件使这更容易做到。

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

    依赖于这些表的表将改变其FK;你也可以 更新行或者 - 可能更简单 - 删除并重新插入 这些行也是。

  3. 创建新架构。这只会创建正在变化的表格。

  4. 编写脚本以使用新密钥读取和重新加载数据。这些很短且非常相似。每个脚本都将使用json.load()从源文件中读取对象;然后,您将从为您构建的JSON元组行对象中创建架构对象。然后,您可以将它们插入数据库。

    你有两个案例。

    • 将插入更改了PK更改的表格,并将获得新的PK。这些必须“级联”到其他表格,以确保其他表格的FK也会发生变化。

    • 更改了FK的表必须在外表中找到该行并更新其FK参考。

  5. 替代。

    1. 重命名所有旧表。

    2. 创建整个新架构。

    3. 编写SQL以将所有数据从旧架构迁移到新架构。这必须巧妙地重新分配密钥。

    4. 删除重命名的旧表。

答案 1 :(得分:8)

要使用south更改主键,可以在datamigration中使用south.db.create_primary_key命令。 要将自定义CharField pk更改为标准AutoField,您应该执行以下操作:

1)在模型中创建新字段

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

1.1)如果你在这个模型的其他模型中有一个外键,也可以在这些模型上创建新的假fk字段(使用IntegerField,然后将其转换)

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

2)创建自动南迁移和迁移:

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

3)创建新的数据迁移

./manage.py datamigration <your_appname> fill_id

在tis datamigration中用数字填充这些新的id和fk字段(只是枚举它们)

    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)在您的模型中,在新的pk字段中设置primary_key = True

id = models.AutoField(primary_key=True)

5)删除旧的主键字段(如果不需要)创建自动迁移和迁移。

5.1)如果你有外键 - 也删除旧的外键字段(迁移)

6)最后一步 - 恢复火关键关系。再次创建真正的fk字段,并删除你的fake_fk字段,创建自动迁移但不要迁移(!) - 你需要修改创建的自动迁移:而不是创建新的fk并删除fake_fk - 重命名列fake_fk

# 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')

所以之前填充的fake_fk变成了一个包含实际关系数据的列,并且在上述所有步骤之后它不会丢失。

答案 2 :(得分:6)

目前你失败了,因为你要添加一个破坏NOT NULL和UNIQUE要求的pk列。

您应该将迁移拆分为several steps,将模式迁移和数据迁移分开:

  • 使用默认值(ddl migration)
  • 添加新列,已编制索引但不是主键
  • 迁移数据:使用正确的值填充新列(数据迁移)
  • 标记新列主键,如果不需要,则删除以前的pk列(ddl migration)

答案 3 :(得分:6)

我今天遇到了同样的问题,并得出了上述答案所启发的解决方案。

我的模型有一个“位置”表。它有一个名为“unique_id”的CharField,我去年愚蠢地把它作为主键。当然,他们并没有像当时预期的那样独特。还有一个“ScheduledMeasurement”模型,它具有“Location”的外键。

现在我想纠正这个错误并给Location一个普通的自动递增主键。

采取的步骤:

  1. 创建CharField ScheduledMeasurement.temp_location_unique_id和模型TempLocation,以及创建它们的迁移。 TempLocation具有我想要的位置结构。

  2. 创建一个数据迁移,使用外键设置所有temp_location_unique_id,并将所有数据从Location复制到TempLocation

  3. 使用迁移

  4. 删除外键和Location表
  5. 按照我想要的方式重新创建Location模型,使用null = True重新创建外键。将'unique_id'重命名为'location_code'...

  6. 创建一个数据迁移,使用TempLocation填充Location中的数据,并使用temp_location填充ScheduledMeasurement中的外键

  7. 在外键中删除temp_location,TempLocation和null = True

  8. 编辑所有假设unique_id唯一的代码(所有objects.get(unique_id = ...)东西),然后使用unique_id ...

答案 4 :(得分:3)

我设法用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>'),
        ]
    

答案 5 :(得分:0)

我自己遇到过这个问题并最终编写了一个可重用(特定于MySQL)的迁移,该迁移也考虑了多对多关系。总结一下,我采取的步骤是:

  1. 像这样修改模型类:

    class Something(models.Model):
        name = models.CharField(max_length=64, unique=True)
    
  2. 沿着以下行添加新迁移:

    app_name = 'app'
    model_name = 'something'
    related_model_name = 'something_else'
    model_table = '%s_%s' % (app_name, model_name)
    pivot_table = '%s_%s_%ss' % (app_name, related_model_name, model_name)
    
    
    class Migration(migrations.Migration):
    
        operations = [
            migrations.AddField(
                model_name=model_name,
                name='id',
                field=models.IntegerField(null=True),
                preserve_default=True,
            ),
            migrations.RunPython(do_most_of_the_surgery),
            migrations.AlterField(
                model_name=model_name,
                name='id',
                field=models.AutoField(
                    verbose_name='ID', serialize=False, auto_created=True,
                    primary_key=True),
                preserve_default=True,
            ),
            migrations.AlterField(
                model_name=model_name,
                name='name',
                field=models.CharField(max_length=64, unique=True),
                preserve_default=True,
            ),
            migrations.RunPython(do_the_final_lifting),
        ]
    

    ,其中

    def do_most_of_the_surgery(apps, schema_editor):
        models = {}
        Model = apps.get_model(app_name, model_name)
    
        # Generate values for the new id column
        for i, o in enumerate(Model.objects.all()):
            o.id = i + 1
            o.save()
            models[o.name] = o.id
    
        # Work on the pivot table before going on
        drop_constraints_and_indices_in_pivot_table()
    
        # Drop current pk index and create the new one
        cursor.execute(
            "ALTER TABLE %s DROP PRIMARY KEY" % model_table
        )
        cursor.execute(
            "ALTER TABLE %s ADD PRIMARY KEY (id)" % model_table
        )
    
        # Rename the fk column in the pivot table
        cursor.execute(
            "ALTER TABLE %s "
            "CHANGE %s_id %s_id_old %s NOT NULL" %
            (pivot_table, model_name, model_name, 'VARCHAR(30)'))
        # ... and create a new one for the new id
        cursor.execute(
            "ALTER TABLE %s ADD COLUMN %s_id INT(11)" %
            (pivot_table, model_name))
    
        # Fill in the new column in the pivot table
        cursor.execute("SELECT id, %s_id_old FROM %s" % (model_name, pivot_table))
        for row in cursor:
            id, key = row[0], row[1]
            model_id = models[key]
    
            inner_cursor = connection.cursor()
            inner_cursor.execute(
                "UPDATE %s SET %s_id=%d WHERE id=%d" %
                (pivot_table, model_name, model_id, id))
    
        # Drop the old (renamed) column in pivot table, no longer needed
        cursor.execute(
            "ALTER TABLE %s DROP COLUMN %s_id_old" %
            (pivot_table, model_name))
    
    def do_the_final_lifting(apps, schema_editor):
        # Create a new unique index for the old pk column
        index_prefix = '%s_id' % model_table
        new_index_prefix = '%s_name' % model_table
        new_index_name = index_name.replace(index_prefix, new_index_prefix)
    
        cursor.execute(
            "ALTER TABLE %s ADD UNIQUE KEY %s (%s)" %
            (model_table, new_index_name, 'name'))
    
        # Finally, work on the pivot table
        recreate_constraints_and_indices_in_pivot_table()
    
    1. 应用新迁移
  3. 您可以在此repo中找到完整的代码。我也在我的blog中写了这篇文章。

答案 6 :(得分:0)

我必须在Django 1.11应用程序中迁移一些键-旧键是基于外部模型的确定性键。不过,后来发现,这个外部模型可能会改变,所以我需要自己的UUID。

作为参考,我正在更改POS专用酒瓶表以及这些酒瓶的销售表。

  • 我在所有相关表上创建了一个额外的字段。第一步,我需要引入可能为None的字段,然后为所有字段生成UUID。接下来,我通过Django进行了更改,其中新的UUID字段被标记为唯一。我可以开始迁移所有视图等,以使用此UUID字段作为查找,这样在即将到来的迁移阶段(可怕的阶段)就需要进行较少的更改。
  • I updated the foreign keys using a join.(在PostgreSQL中,而不是Django)
  • 我将所有提到的旧密钥替换为新密钥,并在单元测试中对其进行了测试,因为它们使用了自己的独立测试数据库。此步骤对于牛仔是可选的。
  • 转到PostgreSQL表,您会注意到外键约束具有带数字的代号。您需要删除这些约束并创建新约束:

    alter table pos_winesale drop constraint pos_winesale_pos_item_id_57022832_fk;
    alter table pos_winesale rename column pos_item_id to old_pos_item_id;
    alter table pos_winesale rename column placeholder_fk to pos_item_id;
    alter table pos_winesale add foreign key (pos_item_id) references pos_poswinebottle (id);
    alter table pos_winesale drop column old_pos_item_id;
    
  • 有了新的外键,您就可以更改主键了,因为不再引用它了:

    alter table pos_poswinebottle drop constraint pos_poswinebottle_pkey;
    alter table pos_poswinebottle add primary key (id);
    alter table pos_poswinebottle drop column older_key;
    
  • Fake the migration history

答案 7 :(得分:0)

我只是尝试了这种方法,它似乎对Django 2.2.2有效,但仅对sqlite有效。在其他数据库(例如postgres SQL)上尝试此方法,但不起作用。

  1. 添加id=models.IntegerField()进行建模,迁移和迁移,并提供一个默认值(如1)

  2. 使用python shell为模型中从1到N的所有对象生成id

  3. 从主键模型中删除primary_key=True,然后删除id=models.IntegerField()。 Makemigration并检查迁移,您应该看到id字段将被迁移到自动字段。

应该可以。

我不知道我如何将主键放入一个字段中,但是如果不确定如何处理主键,我认为最好让Django为您处理。

答案 8 :(得分:0)

我想分享一下我的情况:email列是主键,但是现在是错误的。我需要将主键更改为另一列。在尝试了一些建议之后,我终于想到了最简单的解决方案:

  1. 首先,删除旧的主键。此步骤需要对迁移进行一些自定义:
  • 修改模型,将电子邮件列上的primary_key=True替换为blank=True, null=True
  • 运行makemigrations以创建一个新的迁移文件,并按如下所示对其进行编辑:
class Migration(migrations.Migration):

    dependencies = [
        ('api', '0026_auto_20200619_0808'),
    ]
    operations = [
        migrations.RunSQL("ALTER TABLE api_youth DROP CONSTRAINT api_youth_pkey"),
        migrations.AlterField(
            model_name='youth', name='email',
            field=models.CharField(blank=True, max_length=200, null=True))
    ]

  • 运行迁移
  1. 现在您的表没有主键,您可以添加新列或将旧列用作主键。只需更改模型然后迁移即可。如果需要新列来填充并确保它仅包含唯一值,请执行一些额外的脚本。