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

Posted

技术标签:

【中文标题】在现有 Django 应用程序中更改主键的最佳方法是啥?【英文标题】:What is the best approach to change primary keys in an existing Django app?在现有 Django 应用程序中更改主键的最佳方法是什么? 【发布时间】:2011-01-04 13:48:26 【问题描述】:

我有一个处于 BETA 模式的应用程序。这个应用程序的模型有一些具有显式主键的类。因此 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)

我已经对我的模型进行了更改(将每个 primary_key=True 替换为 db_index=True)并且我想使用 south 迁移数据库。

很遗憾,迁移失败并显示以下消息: ValueError: You cannot add a null=False column without a default value.

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

感谢您的帮助

【问题讨论】:

你能告诉我们你的模型吗? @tomlog : 见***.com/questions/2011629/… 有一个例子。我想将 id 添加为 pk​​ FWIW,只要您的数据库正确使用索引,命名主键并没有错。 【参考方案1】:

我设法通过创建三个迁移来实现这一点。我从以下模型开始:

class MyModel(models.Model):
  id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  created_at = models.DateTimeField(auto_now_add=True)

首先,我们需要一个迁移来重命名主键字段并添加一个新的id占位符IntegerField:

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0001_initial'),
    ]

    operations = [
        migrations.RenameField(
            model_name='mymodel',
            old_name='id',
            new_name='uuid',
        ),
        migrations.AddField(
            model_name='mymodel',
            name='new_id',
            field=models.IntegerField(null=True),
        ),
    ]

现在在下一次迁移中,我们需要按照我们想要的顺序回填id IntegerField(我将使用created_at 时间戳)。

def backfill_pk(apps, schema_editor):
    MyModel = apps.get_model('myapp', 'MyModel')
    curr = 1
    for m in MyModel.objects.all().order_by('created_at'):
        m.new_id = curr
        m.save()
        curr += 1


class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0002_rename_pk'),
    ]

    operations = [
        migrations.RunPython(backfill_pk, reverse_code=migrations.RunPython.noop),
    ]

最后我们需要将uuidid 字段更改为正确的最终配置(注意下面的操作顺序很重要):

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0003_backfill_pk'),
    ]

    operations = [
        migrations.AlterField(
            model_name='mymodel',
            name='uuid',
            field=models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True),
        ),
        migrations.AlterField(
            model_name='mymodel',
            name='new_id',
            field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
        ),
        migrations.RenameField(
            model_name='mymodel',
            old_name='new_id',
            new_name='id',
        ),
    ]

最终的模型状态将如下所示(id 字段在 Django 中是隐含的):

class MyModel(models.Model):
  uuid = models.UUIDField(default=uuid.uuid4, db_index=True, editable=False, unique=True)
  created_at = models.DateTimeField(auto_now_add=True)

【讨论】:

【参考方案2】:

为此处已有的答案添加更多上下文。更改主键:

发件人:

email = models.EmailField(max_length=255, primary_key=True,)

收件人:

id = models.AutoField(auto_created=True, primary_key=True)    
email = models.EmailField(max_length=255,)

创建第一个迁移:

migrations.AddField(
    model_name='my_model',
    name='id',
    field=models.AutoField(auto_created=True, primary_key=True, serialize=False),
    preserve_default=False,
),
migrations.AlterField(
    model_name='my_model',
    name='email',
    field=models.EmailField(max_length=255,),
),

修改迁移 翻转顺序,以便首先修改电子邮件字段。这可以防止“不允许表“my_model”的多个主键”

migrations.AlterField(
    model_name='my_model',
    name='email',
    field=models.EmailField(max_length=255,),
),
migrations.AddField(
    model_name='my_model',
    name='id',
    field=models.AutoField(auto_created=True, primary_key=True, serialize=False),
    preserve_default=False,
),

【讨论】:

这真的很接近。对我来说非常有效,除了我不能对自动增量字段进行任何排序。有人知道如何为 AutoField 提供默认排序吗?我尝试根据我的created_at 字段迁移模型上的排序,但它在 django 级别运行,我相信自动增量发生在 postgres 级别。【参考方案3】:

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

    首先,删除旧的主键。此步骤需要稍微自定义迁移:
编辑模型以将电子邮件列上的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))
    ]

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

【讨论】:

【参考方案4】:

我刚刚尝试过这种方法,它似乎适用于 django 2.2.2,但仅适用于 sqlite。在其他数据库(如 postgres SQL)上尝试此方法但不起作用。

    id=models.IntegerField() 添加到模型、进行迁移和迁移,提供一次性默认值,如 1

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

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

它应该可以工作。

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

【讨论】:

【参考方案5】:

我不得不在我的 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.

【讨论】:

【参考方案6】:

我设法用 django 1.10.4 迁移和 mysql 5.5 做到了这一点,但这并不容易。

我有一个带有多个外键的 varchar 主键。我添加了一个id 字段、迁移的数据和外键。方法如下:

    添加未来的主键字段。我在主模型中添加了一个 id = models.IntegerField(default=0) 字段并生成了自动迁移。

    简单的数据迁移以生成新的主键:

    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)]
    

    迁移现有的外键。我写了一个组合迁移:

    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)
            ),
        ]
    

    交换主键和恢复外键。同样,自定义迁移。当我 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>'),
        ]
    

【讨论】:

【参考方案7】:

同意,您的模型可能是错误的。

正式的主键应始终是代理键。从来没有别的。 【强词夺理。自 1980 年代以来一直是数据库设计师。重要的经验教训是:一切都是可变的,即使用户在母亲的坟墓上发誓价值无法改变,这确实是一把天然的钥匙,可以作为主要的。这不是主要的。只有代理可以是主要的。]

您正在做心脏直视手术。不要搞乱模式迁移。您正在替换架构。​​

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

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

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

    创建新架构。这只会创建正在更改的表。

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

    你有两个案例。

    将插入更改了 PK 的表并获得新的 PK。这些必须“级联”到其他表,以确保其他表的 FK 也被更改。

    具有更改的 FK 的表必须在外部表中找到行并更新其 FK 引用。

另一种选择。

    重命名所有旧表。

    创建整个新架构。

    编写 SQL 将所有数据从旧模式迁移到新模式。这将不得不巧妙地重新分配键。

    删除重命名的旧表。

 

【讨论】:

当你说“explicit primary_key”时,你的意思是不是 Django 定义的正式主键?是的,是的,我同意你的观点。我喜欢你的第一种方法。关于如何在 JSON 中导出/导入的任何指针? 我引用你的问题@luc;您似乎意味着您正在尝试创建 Django 未定义的主键。这是个坏主意。 我已经编辑了我的问题。我希望它带来一些澄清。 dumpdata 和 loaddata 似乎是一种方式。但也许没那么容易 转储数据,重新创建数据库模式和加载数据似乎是个好方法。谢谢 @S.Lott +1 将其称为架构替换 - qua 迁移 - 具有这样的重要性。【参考方案8】:

要使用 south 更改主键,您可以在数据迁移中使用 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 数据迁移中,用数字填充这些新的 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 变成了一列,包含实际的关系数据,并且经过上述所有步骤后它不会丢失。

【讨论】:

你真的试过这个吗?你不能有一个不是主要的自动字段,也不能有一个允许 null 的自动字段(南方不允许,我认为 django 不会)。因此,一旦您更改了主键,您就无法真正进行相关查找。 也就是说,这种方法的改编版本对我来说效果很好。 @marcin,你是如何调整第一步的(克服南/django 对 null、nonpk AutoField 的禁令) 从 0 开始 id 序列会导致 mysql to reassign an ID to that record。所以我建议在第 3 步中使用n+1 来避免这种情况。 有没有人成功地使用这种方法或这种方法的修改版本来更改主键字段?【参考方案9】:

我今天遇到了同样的问题,并根据上面的答案找到了解决方案。

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

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

采取的步骤:

    创建 CharField ScheduledMeasurement.temp_location_unique_id 和模型 TempLocation,并进行迁移以创建它们。 TempLocation 具有我希望 Location 具有的结构。

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

    通过迁移移除外键和位置表

    按照我想要的方式重新创建 Location 模型,使用 null=True 重新创建外键。将“unique_id”重命名为“location_code”...

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

    移除外键中的 temp_location、TempLocation 和 null=True

并编辑所有假定 unique_id 是唯一的代码(所有 objects.get(unique_id=...) 的东西),否则使用 unique_id...

【讨论】:

【参考方案10】:

目前您失败了,因为您添加的 pk 列违反了 NOT NULL 和 UNIQUE 要求。

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

使用默认值添加新列,索引但不是主键(ddl 迁移) 迁移数据:用正确的值填充新列(数据迁移) 标记新列的主键,如果之前的 pk 列变得不必要,则删除它(ddl 迁移)

【讨论】:

由于 South 的问题,这对我不起作用。我建议遵循@RemcoGerlich 或 S.Lott 的解决方案

以上是关于在现有 Django 应用程序中更改主键的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

django中model的主键的设置与自增

创建主键的最佳方法 - Swift

如何安全地更改 Django 模型中主键字段的值?

使用 GUID 作为主键的最佳做法是啥,特别是在性能方面? [关闭]

Django:在不指定主键的情况下创建固定装置?

提取自增主键的标准做法是啥?