51

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

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

我认为这是一个坏主意(请参阅在 django admin 中保存对象时出现 unicode 错误),我想返回并为我的模型的每个类设置一个 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.

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

谢谢你的帮助

4

10 回答 10

94

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

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

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

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

  2. 删除要从旧模式中更改的表。

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

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

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

    你有两个案例。

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

    • FK 发生变化的表必须在外部表中定位行并更新其 FK 引用。

选择。

  1. 重命名所有旧表。

  2. 创建整个新架构。

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

  4. 删除重命名的旧表。

 

于 2010-01-13T11:11:43.053 回答
11

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

于 2012-04-20T17:42:38.003 回答
9

我设法用 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>'),
        ]
    
于 2017-10-01T22:00:30.867 回答
6

目前您失败了,因为您正在添加一个打破 NOT NULL 和 UNIQUE 要求的 pk 列。

您应该将迁移分为几个步骤,将架构迁移和数据迁移分开:

  • 添加具有默认值的新列,索引但不是主键(ddl 迁移)
  • 迁移数据:用正确的值填充新列(数据迁移)
  • 标记新列的主键,如果之前的 pk 列变得不必要,则删除它(ddl 迁移)
于 2010-01-13T10:25:20.950 回答
6

我今天遇到了同样的问题,并在上​​述答案的启发下找到了解决方案。

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

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

采取的步骤:

  1. 创建一个 CharField ScheduledMeasurement.temp_location_unique_id 和一个模型 TempLocation,并通过迁移来创建它们。TempLocation 具有我希望 Location 具有的结构。

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

  3. 使用迁移移除外键和位置表

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

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

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

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

于 2012-09-03T12:08:00.520 回答
3

为已经在这里的答案添加更多上下文。要更改主键:

从:

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,
),
于 2021-08-30T14:51:18.960 回答
1

我不得不在我的 Django 1.11 应用程序中迁移一些键——旧键是确定性的,基于外部模型。但后来,事实证明这个外部模型可能会改变,所以我需要自己的 UUID。

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

  • 我在所有相关表上创建了一个额外的字段。第一步,我需要引入可以为 None 的字段,然后为所有字段生成 UUID。接下来,我通过 Django 应用了一个更改,其中新的 UUID 字段被标记为唯一。我可以开始迁移所有视图等以使用此 UUID 字段作为查找,以便在即将到来的、更可怕的迁移阶段需要更改的内容更少。
  • 我使用连接更新了外键。(在 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;
    
  • 伪造迁移历史

于 2019-01-30T04:31:55.767 回答
1

我刚刚尝试过这种方法,它似乎适用于 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 为你处理它。

于 2019-06-14T07:54:15.980 回答
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. 现在您的表没有主键,您可以添加新列或使用旧列作为主键。只需更改模型然后迁移。如果您需要填充新列并确保它仅包含唯一值,请执行一些额外的脚本。
于 2020-06-24T07:29:30.803 回答
0

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

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

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

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)
于 2021-09-22T13:44:33.850 回答