Перемещение моделей между приложениями Django (1.8) с необходимыми ссылками на ForeignKey
Это расширение к этому вопросу: как перемещать модель между двумя приложениями Django (Django 1.7)
Мне нужно переместить кучу моделей из old_app
в new_app
, Наилучший ответ кажется Озаном, но с необходимыми ссылками на внешние ключи все немного сложнее. @halfnibble представляет решение в комментариях к ответу Озана, но у меня все еще есть проблемы с точным порядком шагов (например, когда я копирую модели в new_app
когда я удаляю модели из old_app
в каких миграциях будет сидеть old_app.migrations
против new_app.migrations
, так далее.)
Любая помощь высоко ценится!
7 ответов
Миграция модели между приложениями.
Короткий ответ: не делай этого!!
Но этот ответ редко работает в реальном мире живых проектов и производственных баз данных. Поэтому я создал пример репозитория GitHub, чтобы продемонстрировать этот довольно сложный процесс.
Я использую MySQL. (Нет, это не мои настоящие полномочия).
Эта проблема
Пример, который я использую - это заводской проект с приложением для автомобилей, которое изначально имеет Car
модель и Tires
модель.
factory
|_ cars
|_ Car
|_ Tires
Car
Модель имеет отношение ForeignKey с Tires
, (Например, вы указываете шины через модель автомобиля).
Однако вскоре мы понимаем, что Tires
будет большой моделью со своими представлениями и т. д., и поэтому мы хотим, чтобы она была в собственном приложении. Поэтому желаемая структура:
factory
|_ cars
|_ Car
|_ tires
|_ Tires
И нам нужно сохранить отношения ForeignKey между Car
а также Tires
потому что слишком многое зависит от сохранения данных.
Решение
Шаг 1. Настройте начальное приложение с плохим дизайном.
Просмотрите код шага 1.
Шаг 2. Создайте интерфейс администратора и добавьте группу данных, содержащих отношения ForeignKey.
Просмотр шаг 2.
Шаг 3. Решите переместить Tires
модель для собственного приложения. Тщательно вырезать и вставлять код в новое приложение шин. Убедитесь, что вы обновили Car
модель, чтобы указать на новый tires.Tires
модель.
Тогда беги ./manage.py makemigrations
и резервное копирование базы данных где-нибудь (на случай, если это ужасно не получается).
Наконец, беги ./manage.py migrate
и увидеть сообщение об ошибке Doom,
django.db.utils.IntegrityError: (1217, "Невозможно удалить или обновить родительскую строку: ограничение внешнего ключа не выполняется")
Просмотрите код и миграцию на шаге 3.
Шаг 4. Хитрая часть. Автоматически сгенерированная миграция не может увидеть, что вы просто скопировали модель в другое приложение. Итак, мы должны сделать некоторые вещи, чтобы исправить это.
Вы можете следить за последними миграциями и просматривать их с комментариями на шаге 4. Я проверил это, чтобы убедиться, что оно работает.
Во-первых, мы будем работать над cars
, Вы должны сделать новую, пустую миграцию. Эта миграция должна быть запущена до самой последней созданной миграции (той, которая не была выполнена). Поэтому я перенумеровал перенесенную мной миграцию и изменил зависимости, чтобы сначала запустить свою пользовательскую миграцию, а затем последнюю автоматически созданную миграцию для cars
приложение.
Вы можете создать пустую миграцию с помощью:
./manage.py makemigrations --empty cars
Шаг 4.а. Сделайте пользовательскую миграцию old_app.
В этой первой пользовательской миграции я собираюсь выполнить миграцию "database_operations". Django дает вам возможность разделить операции "состояние" и "база данных". Вы можете увидеть, как это делается, просмотрев код здесь.
Моя цель на этом первом шаге - переименовать таблицы базы данных из oldapp_model
в newapp_model
не возиться с состоянием Джанго. Вы должны выяснить, как Django назвал бы вашу таблицу базы данных, основываясь на имени приложения и модели.
Теперь вы готовы изменить начальный tires
миграция.
Шаг 4.б. Изменить начальную миграцию new_app
Операции в порядке, но мы хотим изменить только "состояние", а не базу данных. Зачем? Потому что мы храним таблицы базы данных из cars
приложение. Кроме того, вы должны убедиться, что ранее выполненная пользовательская миграция является зависимостью этой миграции. Смотрите файл миграции шин.
Итак, теперь мы переименовали cars.Tires
в tires.Tires
в базе данных, и изменил состояние Django, чтобы распознать tires.Tires
Таблица.
Шаг 4.c. Изменить old_app последней автоматически сгенерированной миграции.
Возвращаясь к машинам, нам нужно изменить эту последнюю автоматически генерируемую миграцию. Это должно потребовать нашей первой миграции автомобилей и первоначальной миграции шин (которую мы только что изменили).
Здесь мы должны оставить AlterField
операции, потому чтоCar
Модель указывает на другую модель (даже если она имеет те же данные). Однако нам нужно убрать линии миграции, касающиеся DeleteModel
посколькуcars.Tires
модель больше не существует Он полностью преобразован в tires.Tires
, Посмотреть эту миграцию.
Шаг 4.d.Уберите устаревшую модель в old_app.
И последнее, но не менее важное: вам нужно сделать окончательную пользовательскую миграцию в приложении Cars. Здесь мы сделаем операцию "состояние" только для удаления cars.Tires
модель. Это только состояние, потому что таблица базы данных для cars.Tires
уже был переименован. Эта последняя миграция очищает оставшееся состояние Джанго.
Только сейчас переехали две модели из old_app
в new_app
, но ссылки FK были в некоторых моделях от app_x
а также app_y
вместо моделей из old_app
,
В этом случае выполните действия, предоставленные Nostalg.io следующим образом:
- Переместить модели из
old_app
вnew_app
затем обновитеimport
заявления по всей базе кода. makemigrations
,- Выполните шаг 4.a. Но использовать
AlterModelTable
для всех перемещенных моделей. Два для меня. - Выполните шаг 4.b. как есть.
- Выполните шаг 4.c. Но также для каждого приложения, в котором есть только что созданный файл миграции, отредактируйте их вручную, чтобы вы перенесли
state_operations
вместо. - Выполните шаг 4.d, но используйте
DeleteModel
для всех перемещенных моделей.
Заметки:
- Все отредактированные автоматически созданные файлы миграции из других приложений зависят от пользовательского файла миграции из
old_app
гдеAlterModelTable
используется для переименования таблиц. (создан на шаге 4.a.) - В моем случае мне пришлось удалить автоматически сгенерированный файл миграции из
old_app
потому что у меня не было никакихAlterField
только операцииDeleteModel
а такжеRemoveField
операции. Или держите это с пустымoperations = []
Чтобы избежать исключений миграции при создании тестовой БД с нуля, убедитесь, что пользовательская миграция с
old_app
создан на шаге 4.a. имеет все предыдущие зависимости миграции от других приложений.old_app 0020_auto_others 0021_custom_rename_models.py dependencies: ('old_app', '0020_auto_others'), ('app_x', '0002_auto_20170608_1452'), ('app_y', '0005_auto_20170608_1452'), ('new_app', '0001_initial'), 0022_auto_maybe_empty_operations.py dependencies: ('old_app', '0021_custom_rename_models'), 0023_custom_clean_models.py dependencies: ('old_app', '0022_auto_maybe_empty_operations'), app_x 0001_initial.py 0002_auto_20170608_1452.py 0003_update_fk_state_operations.py dependencies ('app_x', '0002_auto_20170608_1452'), ('old_app', '0021_custom_rename_models'), app_y 0004_auto_others_that_could_use_old_refs.py 0005_auto_20170608_1452.py 0006_update_fk_state_operations.py dependencies ('app_y', '0005_auto_20170608_1452'), ('old_app', '0021_custom_rename_models'),
Кстати: есть открытый билет по этому поводу: https://code.djangoproject.com/ticket/24686
Я построил команду управления, чтобы сделать это - перенести модель из одного приложения Django в другое - на основе предложений nostalgic.io по адресу /questions/33251933/peremeschenie-modelej-mezhdu-prilozheniyami-django-18-s-neobhodimyimi-ssyilkami-na-foreignkey/33251947#33251947
Вы можете найти его на GitHub по адресу https://github.com/alexei/django-move-model
Вы можете сделать это относительно просто, но вам необходимо выполнить следующие шаги, которые резюмируются из вопроса в группе пользователей Django.
Прежде чем переносить вашу модель в новое приложение, которое мы назовем
new
, добавитьdb_table
вариант для текущей моделиMeta
учебный класс. Мы назовем ту модель, которую вы хотите переместитьM
. Но вы можете делать несколько моделей одновременно, если хотите.class M(models.Model): a = models.ForeignKey(B, on_delete=models.CASCADE) b = models.IntegerField() class Meta: db_table = "new_M"
Пробег
python manage.py makemigrations
. Это создает новый файл миграции, который переименует таблицу в базе данных изcurrent_M
кnew_M
. Мы будем называть этот файл миграцииx
позже.Теперь переместите модели на свой
new
приложение. Удалить ссылку наdb_table
потому что Django автоматически поместит его в таблицу с именемnew_M
.Сделайте новые миграции. Пробег
python manage.py makemigrations
. В нашем примере это сгенерирует два новых файла миграции. Первый будет вnew
приложение. Убедитесь, что в свойстве dependencies Django указалx
из предыдущего файла миграции. Второй будет вcurrent
приложение. Теперь оберните список операций в оба файла миграции при вызовеSeparateDatabaseAndState
быть таким:operations = [ SeparateDatabaseAndState([], [ migrations.CreateModel(...), ... ]), ]
Пробег
python manage.py migrate
. Вы сделали. Это относительно быстро, потому что, в отличие от некоторых ответов, вы не копируете записи из одной таблицы в другую. Вы просто переименовываете таблицы, что само по себе является быстрой операцией.
Если вам нужно переместить модель, и у вас больше нет доступа к приложению (или вам не нужен доступ), вы можете создать новую операцию и рассмотреть возможность создания новой модели, только если перенесенная модель не имеет существовать.
В этом примере я передаю "MyModel" из old_app в myapp.
class MigrateOrCreateTable(migrations.CreateModel):
def __init__(self, source_table, dst_table, *args, **kwargs):
super(MigrateOrCreateTable, self).__init__(*args, **kwargs)
self.source_table = source_table
self.dst_table = dst_table
def database_forwards(self, app_label, schema_editor, from_state, to_state):
table_exists = self.source_table in schema_editor.connection.introspection.table_names()
if table_exists:
with schema_editor.connection.cursor() as cursor:
cursor.execute("RENAME TABLE {} TO {};".format(self.source_table, self.dst_table))
else:
return super(MigrateOrCreateTable, self).database_forwards(app_label, schema_editor, from_state, to_state)
class Migration(migrations.Migration):
dependencies = [
('myapp', '0002_some_migration'),
]
operations = [
MigrateOrCreateTable(
source_table='old_app_mymodel',
dst_table='myapp_mymodel',
name='MyModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=18))
],
),
]
Способ Nostalg.io работал в форвардах (автоматическое создание всех FK приложений, на которые он ссылается). Но мне нужно было и в обратном направлении. Для этого обратный AlterTable должен произойти до того, как любые FK будут возвращены (в оригинале это должно было произойти после этого). Таким образом, для этого я разделил AlterTable на 2 отдельных AlterTableF и AlterTableR, каждый из которых работает только в одном направлении, затем использовал прямую одну вместо оригинальной в первой пользовательской миграции и обратную одну в последней миграции автомобилей (оба происходят в приложении автомобилей). Что-то вроде этого:
#cars/migrations/0002...py :
class AlterModelTableF( migrations.AlterModelTable):
def database_backwards(self, app_label, schema_editor, from_state, to_state):
print( 'nothing back on', app_label, self.name, self.table)
class Migration(migrations.Migration):
dependencies = [
('cars', '0001_initial'),
]
database_operations= [
AlterModelTableF( 'tires', 'tires_tires' ),
]
operations = [
migrations.SeparateDatabaseAndState( database_operations= database_operations)
]
#cars/migrations/0004...py :
class AlterModelTableR( migrations.AlterModelTable):
def database_forwards(self, app_label, schema_editor, from_state, to_state):
print( 'nothing forw on', app_label, self.name, self.table)
def database_backwards(self, app_label, schema_editor, from_state, to_state):
super().database_forwards( app_label, schema_editor, from_state, to_state)
class Migration(migrations.Migration):
dependencies = [
('cars', '0003_auto_20150603_0630'),
]
# This needs to be a state-only operation because the database model was renamed, and no longer exists according to Django.
state_operations = [
migrations.DeleteModel(
name='Tires',
),
]
database_operations= [
AlterModelTableR( 'tires', 'tires_tires' ),
]
operations = [
# After this state operation, the Django DB state should match the actual database structure.
migrations.SeparateDatabaseAndState( state_operations=state_operations,
database_operations=database_operations)
]
После того, как работа была сделана, я попытался сделать новую миграцию. Но я столкнулся со следующей ошибкой:
ValueError: Unhandled pending operations for models:
oldapp.modelname (referred to by fields: oldapp.HistoricalProductModelName.model_ref_obj)
Если ваша модель Django использует HistoricalRecords
не забудьте добавить дополнительные модели / таблицы, следуя @Nostalg.io ответу.
Добавить следующий элемент в database_operations
на первом этапе (4.a):
migrations.AlterModelTable('historicalmodelname', 'newapp_historicalmodelname'),
и добавить дополнительный Удалить в state_operations
на последнем шаге (4.d):
migrations.DeleteModel(name='HistoricalModleName'),
Возвращаясь к этому через пару месяцев (после успешной реализации подхода Лучиановича), мне кажется, что станет намного проще, если вы позаботитесь о том, чтобы указать db_table
к старой таблице (если вы заботитесь только об организации кода и не обращаете внимания на устаревшие имена в базе данных).
- Вам не понадобятся миграции AlterModelTable, поэтому нет необходимости в пользовательском первом шаге.
- Вам все еще нужно изменить модели и отношения, не касаясь базы данных.
Поэтому я просто взял автоматическую миграцию из Django и включил ее в миграцию.SeparateDatabaseAndState.
Обратите внимание (опять же), что это может работать, только если вы позаботились о том, чтобы указать db_table на старую таблицу для каждой модели.
Я не уверен, что с этим что-то не так, чего я пока не вижу, но, похоже, это сработало в моей системе devel (которую я, конечно, позаботился о резервном копировании). Все данные выглядят нетронутыми. Я посмотрю поближе, чтобы проверить, не возникнут ли проблемы...
Возможно, позже можно будет также переименовать таблицы базы данных на отдельном этапе, что сделает весь этот процесс менее сложным.
Это сработало для меня, но я уверен, что услышу, почему это ужасная идея. Добавьте эту функцию и операцию, которая вызывает ее к миграции old_app:
def migrate_model(apps, schema_editor):
old_model = apps.get_model('old_app', 'MovingModel')
new_model = apps.get_model('new_app', 'MovingModel')
for mod in old_model.objects.all():
mod.__class__ = new_model
mod.save()
class Migration(migrations.Migration):
dependencies = [
('new_app', '0006_auto_20171027_0213'),
]
operations = [
migrations.RunPython(migrate_model),
migrations.DeleteModel(
name='MovingModel',
),
]
Шаг 1: сделайте резервную копию вашей базы данных!
Убедитесь, что сначала выполняется миграция new_app, и / или требование миграции old_app. Отмените удаление устаревшего типа контента, пока не завершите миграцию old_app.
после Django 1.9 вы, возможно, захотите пройти более осторожно:
Миграция1: Создать новую таблицу
Миграция2: заполнить таблицу
Миграция3: изменение полей в других таблицах
Migration4: удалить старую таблицу
Это немного поздно, но если вы хотите самый простой путь И не слишком заботитесь о сохранении истории миграции. Простое решение - просто стереть миграции и обновить.
У меня было довольно сложное приложение, и после нескольких часов безуспешной попытки описанных выше решений я понял, что могу просто сделать.
rm cars/migrations/*
./manage.py makemigrations
./manage.py migrate --fake-initial
Престо! Если мне нужно, история миграции все еще находится в Git. А поскольку это, по сути, запретная операция, откат не был проблемой.