Каков наилучший подход для изменения первичных ключей в существующем приложении Django? - PullRequest
31 голосов
/ 13 января 2010

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

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

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

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

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

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

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

Спасибо за вашу помощь

Ответы [ 8 ]

61 голосов
/ 13 января 2010

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

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

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

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

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

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

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

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

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

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

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

Alternative.

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

  2. Создание всей новой схемы.

  3. Напишите SQL для переноса всех данных из старой схемы в новую. Для этого придется ловко переназначать ключи по ходу дела.

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

9 голосов
/ 20 апреля 2012

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

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

3) создать новую миграцию данных

./manage.py datamigration <your_appname> fill_id

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

6 голосов
/ 03 сентября 2012

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

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

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

Предпринятые шаги:

  1. Создание CharField ScheduledMeasurement.temp_location_unique_id и модели TempLocation, а также миграции для их создания.TempLocation имеет структуру, которую я хочу, чтобы Location имел.

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

  3. Удалите внешний ключ и таблицу Location с помощью переноса

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

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

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

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

6 голосов
/ 13 января 2010

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

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

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

Мне удалось сделать это с помощью миграций 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. Замена первичных ключей и восстановление внешних ключей. Опять же, пользовательская миграция. Я автоматически создал базу для этой миграции, когда а) удалил 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>'),
        ]
    
0 голосов
/ 14 июня 2019

Я только что попробовал этот подход, и он, кажется, работает, для django 2.2.2, но работает только для sqlite. Попытка этого метода на другой базе данных, такой как Postgres SQL, но не работает.

  1. Добавьте id=models.IntegerField() к модели, внесите изменения и мигрируйте, предоставьте одно значение по умолчанию, например 1

  2. Использование оболочки python для генерации идентификатора для всех объектов в модели от 1 до N

  3. удалить primary_key=True из модели первичного ключа и удалить id=models.IntegerField(). Выполните миграцию и проверьте миграцию, и вы увидите, что поле id будет перенесено в автозаполнение.

Должно работать.

Я не знал, что я делал с вводом первичного ключа в одно из полей, но если не уверен, как обращаться с первичным ключом, я думаю, что лучше позволить Джанго позаботиться об этом за вас.

0 голосов
/ 30 января 2019

Мне пришлось перенести некоторые ключи в моем приложении 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;
    
  • Поддельная история миграции .

0 голосов
/ 09 сентября 2017

Я столкнулся с этой проблемой сам и в итоге написал многоразовую (специфичную для 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. Применить новую миграцию

Вы можете найти полный код в этом репо . Я также написал об этом в моем блоге .

...