Каков наилучший подход к изменению первичных ключей в существующем приложении 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.
Я оцениваю различные обходные пути для этой проблемы. Любые предложения?
Спасибо за помощь
Ответы
Ответ 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()
- Применить новую миграцию
Вы можете найти полный код в этом 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, но не работает.
-
Добавьте id=models.IntegerField()
к модели, внесите изменения и перенастройте, предоставьте одно значение по умолчанию, например 1
-
Использование оболочки Python для генерации идентификатора для всех объектов в модели от 1 до N
-
удалить primary_key=True
из модели первичного ключа и удалить id=models.IntegerField()
. Сделайте миграцию и проверьте миграцию, и вы должны увидеть, что поле id будет перенесено в автозаполнение.
Он должен работать.
Я не знал, что я делал с вводом первичного ключа в одно из полей, но если не уверен, как обращаться с первичным ключом, я думаю, что лучше позволить Джанго позаботиться об этом за вас.