Перемещение моделей между приложениями Django (1.8) с требуемыми ссылками ForeignKey
Это расширение для этого вопроса: Как перемещать модель между двумя приложениями Django (Django 1.7)
Мне нужно переместить кучу моделей от old_app
до new_app
. Лучший ответ, кажется, Ozan's, но с необходимыми ссылками на внешние ключи, вещи немного сложнее. @halfnibble представляет решение в комментариях к ответу Озана, но у меня все еще возникают проблемы с точным порядком шагов (например, когда я копирую модели до new_app
, когда я удаляю модели из old_app
, какие миграции будут находиться в old_app.migrations
и new_app.migrations
и т.д.)
Любая помощь очень ценится!
Ответы
Ответ 1
Миграция модели между приложениями.
Короткий ответ: не делайте этого!
Но этот ответ редко работает в реальном мире живых проектов и производственных баз данных. Поэтому я продемонстрировал образец GitHub repo, чтобы продемонстрировать этот довольно сложный процесс.
Я использую MySQL. (Нет, это не мои настоящие верительные грамоты).
Проблема
Пример, который я использую, - это проект factory с приложением cars, изначально имеющим модель 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.a. Выполнение пользовательской миграции old_app.
В этой первой пользовательской миграции я собираюсь выполнить миграцию "database_operations". Django дает вам возможность разделить операции "состояние" и "база данных". Вы можете увидеть, как это делается, просмотрев здесь.
Моя цель на этом первом этапе - переименовать таблицы базы данных от oldapp_model
до newapp_model
, не войдя в состояние Django. Вы должны выяснить, как Django назвал бы вашу таблицу базы данных на основе имени приложения и имени модели.
Теперь вы готовы изменить начальную миграцию Tires
.
Шаг 4.b. Изменить первоначальную миграцию 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.Tires
. Это состояние только потому, что таблица базы данных для cars.Tires
уже переименована. Эта последняя миграция очищает оставшееся состояние Django.
Ответ 2
Теперь мы переместили две модели из 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
Ответ 3
Если вам нужно переместить модель, и у вас больше нет доступа к приложению (или вам не нужен доступ), вы можете создать новую операцию и подумать о создании новой модели, только если перенесенная модель не существует.
В этом примере я передаю "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))
],
),
]
Ответ 4
После завершения работы я попытался выполнить новую миграцию. Но я столкнулся со следующей ошибкой:
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'),
Ответ 5
Это сработало для меня, но я уверен, что я услышу, почему это ужасная идея. Добавьте эту функцию и операцию, которая вызывает ее для миграции 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 вам может понадобиться сделать несколько более осторожно:
Migration1: создать новый стол
Миграция2: заполнить таблицу
Migration3: изменить поля на других таблицах
Миграция4: удаление старой таблицы
Ответ 6
Способ 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)
]
Ответ 7
Я построил команду управления, чтобы сделать это - перенести модель из одного приложения Django в другое - на основе предложений nostalgic.io на fooobar.com/questions/31684/...
Вы можете найти его на GitHub по адресу alexei/django-move-model
Ответ 8
Возвращаясь к этому через пару месяцев (после успешной реализации подхода Lucianovici), мне кажется, что станет намного проще, если вы позаботитесь указать db_table
на старую таблицу (если вы заботитесь только об организации кода и не обращайте внимания на устаревшие имена в базе данных).
- Вам не понадобятся миграции AlterModelTable, поэтому нет необходимости в настраиваемом первом шаге.
- Вам все еще нужно изменить модели и отношения, не касаясь базы данных.
Итак, я просто взял автоматические миграции из Django и превратил их в migrations.SeparateDatabaseAndState.
Обратите внимание (опять же), что это может работать, только если вы позаботились о том, чтобы указать db_table на старую таблицу для каждой модели.
Я не уверен, что с этим что-то не так, чего я пока не вижу, но, похоже, это сработало в моей системе devel (которую я, конечно, позаботился о резервном копировании). Все данные выглядят нетронутыми. Я посмотрю поближе, чтобы проверить, не возникнут ли какие-либо проблемы...
Возможно, позже можно будет также переименовать таблицы базы данных на отдельном этапе, что сделает весь этот процесс менее сложным.
Ответ 9
Это будет немного поздно, но если вы хотите самый простой путь И не слишком заботитесь о сохранении своей истории миграции. Простое решение - просто стереть миграции и обновить.
У меня было довольно сложное приложение, и после нескольких часов безуспешных попыток, описанных выше, я понял, что могу это сделать.
rm cars/migrations/*
./manage.py makemigrations
./manage.py migrate --fake-initial
Presto! История миграции все еще в Git, если мне это нужно. И так как это по сути неактивно, откат не был проблемой.
Ответ 10
Вы можете сделать это относительно просто, но вам нужно выполнить следующие шаги, которые обобщены из вопроса в группе пользователей 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
. Убедитесь, что в свойстве зависимостей Django перечислил x
из предыдущего файла миграции. Второй будет в приложении current
. Теперь оберните список операций в обоих файлах миграции при вызове SeparateDatabaseAndState
так:
operations = [
SeparateDatabaseAndState([], [
migrations.CreateModel(...), ...
]),
]
Запустите python manage.py migrate
. Вы сделали. Время сделать это относительно быстро, потому что, в отличие от некоторых ответов, вы не копируете записи из одной таблицы в другую. Вы просто переименовываете таблицы, что само по себе является быстрой операцией.