Как перенести модель из одного приложения django в новое?

126

У меня есть приложение django с четырьмя моделями. Теперь я понимаю, что одна из этих моделей должна быть в отдельном приложении. У меня действительно установлен юг для миграций, но я не думаю, что он может справиться с этим автоматически. Как перенести одну из моделей из старого приложения в новое?

Также имейте в виду, что мне нужно, чтобы это был повторяемый процесс, чтобы я мог перенести производственную систему и тому подобное.

Apreche
источник
6
Для Джанго 1.7 и выше см stackoverflow.com/questions/25648393/...
Рик Westera

Ответы:

184

Как мигрировать, используя юг.

Допустим, у нас есть два приложения: общее и конкретное:

myproject/
|-- common
|   |-- migrations
|   |   |-- 0001_initial.py
|   |   `-- 0002_create_cat.py
|   `-- models.py
`-- specific
    |-- migrations
    |   |-- 0001_initial.py
    |   `-- 0002_create_dog.py
    `-- models.py

Теперь мы хотим переместить модель common.models.cat в конкретное приложение (а именно в specific.models.cat). Сначала внесите изменения в исходный код, а затем запустите:

$ python manage.py schemamigration specific create_cat --auto
 + Added model 'specific.cat'
$ python manage.py schemamigration common drop_cat --auto
 - Deleted model 'common.cat'

myproject/
|-- common
|   |-- migrations
|   |   |-- 0001_initial.py
|   |   |-- 0002_create_cat.py
|   |   `-- 0003_drop_cat.py
|   `-- models.py
`-- specific
    |-- migrations
    |   |-- 0001_initial.py
    |   |-- 0002_create_dog.py
    |   `-- 0003_create_cat.py
    `-- models.py

Теперь нам нужно отредактировать оба файла миграции:

#0003_create_cat: replace existing forward and backward code
#to use just one sentence:

def forwards(self, orm):
    db.rename_table('common_cat', 'specific_cat') 

    if not db.dry_run:
        # For permissions to work properly after migrating
        orm['contenttypes.contenttype'].objects.filter(
            app_label='common',
            model='cat',
        ).update(app_label='specific')

def backwards(self, orm):
    db.rename_table('specific_cat', 'common_cat')

    if not db.dry_run:
        # For permissions to work properly after migrating
        orm['contenttypes.contenttype'].objects.filter(
            app_label='specific',
            model='cat',
        ).update(app_label='common')

#0003_drop_cat:replace existing forward and backward code
#to use just one sentence; add dependency:

depends_on = (
    ('specific', '0003_create_cat'),
)
def forwards(self, orm):
    pass
def backwards(self, orm):
    pass

Теперь оба миграции приложений знают об изменениях, и жизнь - отстой чуть меньше :-) Установка этой взаимосвязи между миграциями - ключ к успеху. Теперь, если вы это сделаете:

python manage.py migrate common
 > specific: 0003_create_cat
 > common: 0003_drop_cat

выполнит как миграцию, так и

python manage.py migrate specific 0002_create_dog
 < common: 0003_drop_cat
 < specific: 0003_create_cat

перенесет вещи вниз.

Обратите внимание, что для обновления схемы я использовал обычное приложение, а для перехода на более раннюю версию я использовал конкретное приложение. Это потому, что здесь работает зависимость.

Потр Чачур
источник
1
Вау, спасибо. Я изучил юг самостоятельно с тех пор, как задал этот вопрос, но я уверен, что это очень поможет другим.
Apreche
11
Вам также может потребоваться выполнить миграцию данных в таблице django_content_type.
spookylukey
1
Действительно отличный гид @Potr. Мне любопытно, а orm['contenttypes.contenttype'].objects.filter в обратной части 0003_create_catтоже не должно быть линии ? Также хочу поделиться советом. Если у вас есть индексы, их тоже нужно будет изменить. В моем случае это были уникальные индексы, поэтому мой прогноз выглядит так: db.delete_unique('common_cat', ['col1']) db.rename_table('common_cat', 'specific_cat') db.delete_unique('specific_cat', ['col1'])
Брэд Питчер,
2
Чтобы получить доступ orm['contenttypes.contenttype'], вам также необходимо добавить --freeze contenttypesпараметр в свои schemamigrationкоманды.
Гэри
1
В моем случае (Django 1.5.7 и South 1.0) .. Мне пришлось ввести, python manage.py schemamigration specific create_cat --auto --freeze commonчтобы получить доступ к модели кошки из общего приложения.
geoom
35

Для того, чтобы построить на Potr Czachur «s ответ , ситуации, связанные с ForeignKeys более сложными и должны быть обработаны немного по- другому.

(Следующий пример основывается на commonи specificпрограммы упомянутого к в текущем ответ).

# common/models.py

class Cat(models.Model):
    # ...

class Toy(models.Model):
    belongs_to = models.ForeignKey(Cat)
    # ...

затем изменится на

# common/models.py

from specific.models import Cat

class Toy(models.Model):
    belongs_to = models.ForeignKey(Cat)
    # ...

# specific/models.py

class Cat(models.Model):
    # ...

Бег

./manage.py schemamigration common --auto
./manage.py schemamigration specific --auto # or --initial

будет генерировать следующие миграции (я намеренно игнорирую изменения Django ContentType - см. ранее упомянутый ответ, чтобы узнать, как с этим справиться):

# common/migrations/0009_auto__del_cat.py

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.delete_table('common_cat')
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['specific.Cat']))

    def backwards(self, orm):
        db.create_table('common_cat', (
            # ...
        ))
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['common.Cat']))

# specific/migrations/0004_auto__add_cat.py

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.create_table('specific_cat', (
            # ...
        ))

    def backwards(self, orm):
        db.delete_table('specific_cat')

Как видите, FK необходимо изменить, чтобы он ссылался на новую таблицу. Нам нужно добавить зависимость, чтобы мы знали порядок, в котором будут применяться миграции (и, таким образом, таблица будет существовать до того, как мы попытаемся добавить к ней FK), но мы также должны убедиться, что откат назад тоже работает, потому что зависимость применяется в обратном направлении .

# common/migrations/0009_auto__del_cat.py

class Migration(SchemaMigration):

    depends_on = (
        ('specific', '0004_auto__add_cat'),
    )

    def forwards(self, orm):
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['specific.Cat']))

    def backwards(self, orm):
        db.rename_table('specific_cat', 'common_cat')
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['common.Cat']))

# specific/migrations/0004_auto__add_cat.py

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.rename_table('common_cat', 'specific_cat')

    def backwards(self, orm):
        pass

Согласно документации Южной , depends_onбудет гарантировать , что 0004_auto__add_catбежит перед 0009_auto__del_cat при переходе вперед , но в порядке , обратном порядку при переходе в обратном направлении . Если мы оставили db.rename_table('specific_cat', 'common_cat')в specificоткате, то commonоткат потерпит неудачу при попытке перенести ForeignKey , так как таблица ссылочной таблица не существует.

Надеюсь, это ближе к «реальной ситуации», чем существующие решения, и кто-то сочтет это полезным. Ура!

Мэтт Бриансон
источник
Фиксированные источники в этом ответе опускают строки для обновления типов контента, которые присутствуют в ответе Потра Чахура. Это могло ввести в заблуждение.
Шай Бергер
@ShaiBerger Я обратился к этому конкретно: «Я намеренно игнорирую изменения Django ContentType - см. Ранее упомянутый ответ, чтобы узнать, как с этим справиться».
Мэтт Бриансон
9

Модели не очень тесно связаны с приложениями, поэтому перемещать их довольно просто. Django использует имя приложения в имени таблицы базы данных, поэтому, если вы хотите переместить свое приложение, вы можете либо переименовать таблицу базы данных с помощью оператора SQL ALTER TABLE, либо, что еще проще, просто использовать db_tableпараметр в Metaклассе вашей модели для ссылки на старое имя.

Если вы до сих пор использовали ContentTypes или общие отношения где-либо в вашем коде, вы, вероятно, захотите переименовать app_labelтип контента, указывающий на движущуюся модель, чтобы существующие отношения были сохранены.

Конечно, если у вас вообще нет данных, которые нужно сохранить, проще всего полностью удалить таблицы базы данных и запустить их ./manage.py syncdbснова.

Дэниел Розман
источник
2
Как мне это сделать с миграцией на юг?
Apreche
4

Вот еще одно исправление отличного решения Потра. Добавьте следующее к конкретному / 0003_create_cat

depends_on = (
    ('common', '0002_create_cat'),
)

Если эта зависимость не установлена Юг не будет гарантировать , что common_catтаблица существует в то время , когда конкретный / 0003_create_cat запускается, бросая django.db.utils.OperationalError: no such table: common_catошибку на вас.

South выполняет миграции в лексикографическом порядке, если явно не задана зависимость. Поскольку это commonпроисходит до того, как specificвсе commonмиграции будут выполняться перед переименованием таблицы, вероятно, он не будет воспроизводиться в исходном примере, показанном Потром. Но если переименовать commonв app2и specificк app1вам будет столкнуться с этой проблемой.

Игорь Кагарличенко
источник
На самом деле это не проблема с примером Потра. Он использует конкретную миграцию для переименования и общую миграцию в зависимости от конкретной. Если конкретный запускается первым, все в порядке. Если общий запускается первым, зависимость выполнит конкретный запуск перед ним. Тем не менее, при этом я поменял местами порядок, поэтому переименование произошло совместно, а зависимость - в частности, а затем вам нужно изменить зависимость, как вы описали выше.
Emil Stenström
1
Я не могу с тобой согласиться. С моей точки зрения, решение должно быть надежным и работать, не пытаясь ему угодить. Исходное решение не работает, если вы начнете со свежей базы данных и файла syncdb / migrate. Мое предложение это исправляет. Так или иначе, ответ Порта сэкономил мне много времени, слава ему :)
Игорь Кагарличенко
Если этого не сделать, тесты тоже могут потерпеть неудачу (кажется, что при создании своей тестовой базы данных всегда выполняется полная южная миграция). Я делал нечто подобное раньше. Хороший улов, Игорь :)
odinho - Velmont 08
4

Процесс, на котором я остановился, так как я был здесь несколько раз и решил формализовать его.

Это был первоначально построен на ответ Potr Czachur в и ответ Мэтт Бриансона в , используя Южный 0.8.4

Шаг 1. Обнаружение дочерних внешних ключей

# Caution: This finds OneToOneField and ForeignKey.
# I don't know if this finds all the ways of specifying ManyToManyField.
# Hopefully Django or South throw errors if you have a situation like that.
>>> Cat._meta.get_all_related_objects()
[<RelatedObject: common:toy related to cat>,
 <RelatedObject: identity:microchip related to cat>]

Итак, в этом расширенном случае мы обнаружили другую связанную модель, например:

# Inside the "identity" app...
class Microchip(models.Model):

    # In reality we'd probably want a ForeignKey, but to show the OneToOneField
    identifies = models.OneToOneField(Cat)

    ...

Шаг 2. Создайте миграции

# Create the "new"-ly renamed model
# Yes I'm changing the model name in my refactoring too.
python manage.py schemamigration specific create_kittycat --auto

# Drop the old model
python manage.py schemamigration common drop_cat --auto

# Update downstream apps, so South thinks their ForeignKey(s) are correct.
# Can skip models like Toy if the app is already covered
python manage.py schemamigration identity update_microchip_fk --auto

Шаг 3. Контроль версий: зафиксируйте изменения.

Делает процесс более повторяемым, если вы сталкиваетесь с конфликтами слияния, например, когда товарищи по команде пишут миграции в обновленных приложениях.

Шаг 4. Добавьте зависимости между миграциями.

В основном create_kittycatзависит от текущего состояния всего, а потом все зависит от create_kittycat.

# create_kittycat
class Migration(SchemaMigration):

    depends_on = (
        # Original model location
        ('common', 'the_one_before_drop_cat'),

        # Foreign keys to models not in original location
        ('identity', 'the_one_before_update_microchip_fk'),
    )
    ...


# drop_cat
class Migration(SchemaMigration):

    depends_on = (
        ('specific', 'create_kittycat'),
    )
    ...


# update_microchip_fk
class Migration(SchemaMigration):

    depends_on = (
        ('specific', 'create_kittycat'),
    )
    ...

Шаг 5. Изменение переименования таблицы, которое мы хотим внести.

# create_kittycat
class Migration(SchemaMigration):

    ...

    # Hopefully for create_kittycat you only need to change the following
    # 4 strings to go forward cleanly... backwards will need a bit more work.
    old_app = 'common'
    old_model = 'cat'
    new_app = 'specific'
    new_model = 'kittycat'

    # You may also wish to update the ContentType.name,
    # personally, I don't know what its for and
    # haven't seen any side effects from skipping it.

    def forwards(self, orm):

        db.rename_table(
            '%s_%s' % (self.old_app, self.old_model),
            '%s_%s' % (self.new_app, self.new_model),
        )

        if not db.dry_run:
            # For permissions, GenericForeignKeys, etc to work properly after migrating.
            orm['contenttypes.contenttype'].objects.filter(
                app_label=self.old_app,
                model=self.old_model,
            ).update(
                app_label=self.new_app,
                model=self.new_model,
            )

        # Going forwards, should be no problem just updating child foreign keys
        # with the --auto in the other new South migrations

    def backwards(self, orm):

        db.rename_table(
            '%s_%s' % (self.new_app, self.new_model),
            '%s_%s' % (self.old_app, self.old_model),
        )

        if not db.dry_run:
            # For permissions, GenericForeignKeys, etc to work properly after migrating.
            orm['contenttypes.contenttype'].objects.filter(
                app_label=self.new_app,
                model=self.new_model,
            ).update(
                app_label=self.old_app,
                model=self.old_model,
            )

        # Going backwards, you probably should copy the ForeignKey
        # db.alter_column() changes from the other new migrations in here
        # so they run in the correct order.
        #
        # Test it! See Step 6 for more details if you need to go backwards.
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['common.Cat']))
        db.alter_column('identity_microchip', 'identifies_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['common.Cat']))


# drop_cat
class Migration(SchemaMigration):

    ...

    def forwards(self, orm):
        # Remove the db.delete_table(), if you don't at Step 7 you'll likely get
        # "django.db.utils.ProgrammingError: table "common_cat" does not exist"

        # Leave existing db.alter_column() statements here
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['specific.KittyCat']))

    def backwards(self, orm):
        # Copy/paste the auto-generated db.alter_column()
        # into the create_kittycat migration if you need backwards to work.
        pass


# update_microchip_fk
class Migration(SchemaMigration):

    ...

    def forwards(self, orm):
        # Leave existing db.alter_column() statements here
        db.alter_column('identity_microchip', 'identifies_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['specific.KittyCat']))

    def backwards(self, orm):
        # Copy/paste the auto-generated db.alter_column()
        # into the create_kittycat migration if you need backwards to work.
        pass

Шаг 6. Только если вам нужно backwards () для работы И получить KeyError, работающую в обратном направлении.

# the_one_before_create_kittycat
class Migration(SchemaMigration):

    # You many also need to add more models to South's FakeORM if you run into
    # more KeyErrors, the trade-off chosen was to make going forward as easy as
    # possible, as that's what you'll probably want to do once in QA and once in
    # production, rather than running the following many times:
    #
    # python manage.py migrate specific <the_one_before_create_kittycat>

    models = {
        ...
        # Copied from 'identity' app, 'update_microchip_fk' migration
        u'identity.microchip': {
            'Meta': {'object_name': 'Microchip'},
            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
            'identifies': ('django.db.models.fields.related.OneToOneField', [], {to=orm['specific.KittyCat']})
        },
        ...
    }

Шаг 7. Протестируйте - того, что мне подходит, может не хватить для вашей реальной жизненной ситуации :)

python manage.py migrate

# If you need backwards to work
python manage.py migrate specific <the_one_before_create_kittycat>
pzrq
источник
3

Таким образом, использование исходного ответа от @Potr выше не сработало для меня на South 0.8.1 и Django 1.5.1. Я публикую ниже то, что сработало для меня, в надежде, что это поможет другим.

from south.db import db
from south.v2 import SchemaMigration
from django.db import models

class Migration(SchemaMigration):

    def forwards(self, orm):
        db.rename_table('common_cat', 'specific_cat') 

        if not db.dry_run:
             db.execute(
                "update django_content_type set app_label = 'specific' where "
                " app_label = 'common' and model = 'cat';")

    def backwards(self, orm):
        db.rename_table('specific_cat', 'common_cat')
            db.execute(
                "update django_content_type set app_label = 'common' where "
                " app_label = 'specific' and model = 'cat';")
Тим Саттон
источник
1

Я собираюсь дать более точную версию одной из вещей, предложенных Дэниелом Розманом в своем ответе ...

Если вы просто измените db_tableатрибут Meta модели, которую вы переместили, чтобы он указывал на существующее имя таблицы (вместо нового имени, которое Django даст ему, если вы удалите и сделали a syncdb), вы можете избежать сложных миграций на юг. например:

Оригинал:

# app1/models.py
class MyModel(models.Model):
    ...

После переезда:

# app2/models.py
class MyModel(models.Model):
    class Meta:
        db_table = "app1_mymodel"

Теперь вам просто нужно выполнить миграцию данных, чтобы обновить app_labelfor MyModelв django_content_typeтаблице, и все должно быть в порядке ...

Запустите, ./manage.py datamigration django update_content_typeзатем отредактируйте файл, который South создает для вас:

def forwards(self, orm):
    moved = orm.ContentType.objects.get(app_label='app1', model='mymodel')
    moved.app_label = 'app2'
    moved.save()

def backwards(self, orm):
    moved = orm.ContentType.objects.get(app_label='app2', model='mymodel')
    moved.app_label = 'app1'
    moved.save()
Anentropic
источник