Перемещение моделей между приложениями 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.

  1. Прежде чем переносить вашу модель в новое приложение, которое мы назовем 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"
    
  2. Пробег python manage.py makemigrations. Это создает новый файл миграции, который переименует таблицу в базе данных изcurrent_M к new_M. Мы будем называть этот файл миграцииx позже.

  3. Теперь переместите модели на свой newприложение. Удалить ссылку наdb_table потому что Django автоматически поместит его в таблицу с именем new_M.

  4. Сделайте новые миграции. Пробегpython manage.py makemigrations. В нашем примере это сгенерирует два новых файла миграции. Первый будет вnewприложение. Убедитесь, что в свойстве dependencies Django указалxиз предыдущего файла миграции. Второй будет вcurrentприложение. Теперь оберните список операций в оба файла миграции при вызовеSeparateDatabaseAndState быть таким:

    operations = [
        SeparateDatabaseAndState([], [
            migrations.CreateModel(...), ...
        ]),
    ]
    
  5. Пробег 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. А поскольку это, по сути, запретная операция, откат не был проблемой.

Другие вопросы по тегам