Каков наилучший подход к изменению первичных ключей в существующем приложении Django?

у меня есть приложение, которое находится в бета-режиме. Модель этого приложения имеет некоторые классы с явным primary_key. Как следствие, Django использует поля и не создает идентификатор автоматически.

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

Я думаю, что это была плохая идея (см. ошибка unicode при сохранении объекта в Django admin) и я хотел бы вернуться и иметь идентификатор для каждого класса в моей модели.

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

я внес изменения в свою модель (замените все primary_key=True by db_index=True), и я хочу перенести базу данных с Южный.

к сожалению, миграция завершается со следующим сообщением: ValueError: You cannot add a null=False column without a default value.

Я оцениваю различные обходные пути для этой проблемы. Есть предложения?

Спасибо за помощь

6 ответов


согласен, ваша модель, вероятно, ошибочна.

формальный первичный ключ всегда должен быть суррогатный ключ. Больше ничего. [Сильные слова. Был дизайнером баз данных с 1980-х годов. Важно усвоить следующее: все изменчиво, даже когда пользователи клянутся на могилах своих матерей, что ценность не может быть изменена, это действительно естественный ключ, который можно принять за первичный. Это не главное. Только суррогаты могут быть первичными.]

вы делаете открытое сердце хирургия. Не связывайтесь с миграцией схемы. Ты замена схемы.

  1. выгрузите данные в файлы JSON. Используйте собственный внутренний Django django-admin.py инструменты для этого. Вы должны создать один файл выгрузки для каждого, который будет меняться, и каждой таблицы, которая зависит от ключа, который создается. Отдельные файлы делают это несколько проще.

  2. отбросьте таблицы, которые вы собираетесь изменить из старого схема.

    таблицы, которые зависят от этих таблиц, будут иметь измененные FK; вы можете либо обновите строки на месте или-это может быть проще-удалить и повторно вставить и эти ряды тоже.

  3. создать новую схему. Это создаст только изменяющиеся таблицы.

  4. написать сценарии для чтения и перезагрузки данных с помощью новых ключей. Они короткие и очень похожие. Каждый скрипт будет использовать json.load() для чтения объектов из исходный файл; затем вы создадите объекты схемы из объектов кортежа JSON, которые были созданы для вас. Затем их можно вставить в базу данных.

    у вас есть два случая.

    • таблицы с изменением PK будут вставлены и получат новые PK. Они должны быть "каскадированы" на другие таблицы, чтобы гарантировать, что FK другой таблицы также изменится.

    • таблицы с FK, которые изменятся, должны будут найти строку в внешняя таблица и обновление их ссылки FK.

альтернативы.

  1. переименуйте все ваши старые таблицы.

  2. создать всю новую схему.

  3. писать SQL для переноса всех данных из старой схемы в новую схему. Это должно будет ловко переназначить ключи, как это происходит.

  4. отбросьте переименованный старый таблицы.


чтобы изменить первичный ключ с юга, вы можете использовать юг.децибел.команда create_primary_key в datamigration. Чтобы изменить пользовательский 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) Создайте автоматическую Южную миграцию и migrate:

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

3) создать новый datamigration

./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) в ваших моделях установите primary_key=True в новом поле pk

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 становится столбцом, который содержит фактические данные отношения, и он не теряется после всех шагов выше.


В настоящее время вы терпите неудачу, потому что вы добавляете столбец pk, который нарушает не нулевые и уникальные требования.

вы должны разделить миграцию на несколько шагов, разделяя миграции схемы и миграции данных:

  • добавьте новый столбец, индексированный, но не первичный ключ, со значением по умолчанию (миграция ddl)
  • перенос данных: заполните новый столбец правильным значением (перенос данных)
  • отметить новый столбец первичный ключ и удалите бывший столбец pk, если он стал ненужным (миграция ddl)

У меня была та же проблема сегодня и пришел к решению, вдохновленному ответами выше.

моя модель имеет таблицу "местоположение". У него есть CharField под названием "unique_id", и я глупо сделал его первичным ключом в прошлом году. Конечно, они оказались не такими уникальными, как ожидалось в то время. Существует также модель "ScheduledMeasurement", которая имеет внешний ключ к"местоположению".

теперь я хочу исправить эту ошибку и дать место обычный автоинкрементные первичные ключ.

меры:

  1. создать Charfield ScheduledMeasurement.temp_location_unique_id и TempLocation модели и миграции для их создания. TempLocation имеет структуру, которую я хочу иметь.

  2. создайте миграцию данных, которая устанавливает все temp_location_unique_id с помощью внешнего ключа и копирует все данные из местоположения в TempLocation

  3. удалить внешний ключ и Таблица местоположения с миграцией

  4. повторно создайте модель местоположения так, как я хочу, повторно создайте внешний ключ с null=True. Переименован в "unique_id" в "location_code"...

  5. создайте миграцию данных, которая заполняет данные в местоположении с помощью TempLocation и заполняет внешние ключи в ScheduledMeasurement с помощью temp_location

  6. удалить temp_location, TempLocation и null=True во внешнем ключ

и отредактируйте весь код, который предполагал, что unique_id был уникальным (все объекты.get (unique_id=...) stuff), и это использовало unique_id в противном случае...


мне удалось сделать это с миграциями 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 из старого первичного ключа и Б) удалены 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>'),
        ]
    

Я сам столкнулся с этой проблемой и в конечном итоге написал многоразовую (специфичную для MySQL) миграцию, которая также учитывает отношения "многие ко многим". В заключение я сделал следующие шаги:--4-->

  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. применить новые миграционные

вы можете найти полный код в этом РЕПО. Я также написал об этом в своем блог.