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

  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. Убедитесь, что в свойстве зависимостей Django перечислил x из предыдущего файла миграции. Второй будет в приложении current. Теперь оберните список операций в обоих файлах миграции при вызове SeparateDatabaseAndState так:

    operations = [
        SeparateDatabaseAndState([], [
            migrations.CreateModel(...), ...
        ]),
    ]
    
  5. Запустите python manage.py migrate. Вы сделали. Время сделать это относительно быстро, потому что, в отличие от некоторых ответов, вы не копируете записи из одной таблицы в другую. Вы просто переименовываете таблицы, что само по себе является быстрой операцией.