Подтвердить что ты не робот

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

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

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

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

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.

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

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

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

Alternative.

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

  • Создайте всю новую схему.

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

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

 

Ответ 2

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

Ответ 3

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

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

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

Ответ 4

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

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

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

Сделанные шаги:

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

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

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

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

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

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

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

Ответ 5

Мне удалось сделать это с помощью django 1.10.4 migulations и 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>'),
        ]
    

Ответ 6

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

  • Измените класс модели следующим образом:

    class Something(models.Model):
        name = models.CharField(max_length=64, unique=True)
    
  • Добавьте новую миграцию в следующие строки:

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

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

Ответ 7

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

Ответ 8

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

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

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

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

Он должен работать.

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