Отмена дата-миграций

Помимо обычных миграций есть ещё дата-миграции. Они не меняют структуру таблиц в БД, но изменяют данные в БД.

Ниже приведён пример дата-миграции на сайте с блогом. У поста есть два поля: author и owner, перед вами стоит задача перенести данные из старого поля owner в новое author. Именно это делает функция move_owners_to_authors:

from django.db import migrations


def move_owners_to_authors(apps, schema_editor):
    Post = apps.get_model('blog', 'Post')
    for post in Post.objects.all():
        post.author = post.owner
        post.owner = None
        post.save()


class Migration(migrations.Migration):

    dependencies = [
        ('appname', '0007_auto_20190808_1301'),
    ]

    operations = [
        migrations.RunPython(move_owners_to_authors),
    ]

При создании обычных миграций (их называют схема-миграции) создаются как бы две миграции сразу. Одна меняет схему таблиц, а другая — зеркальная, описывает как вернуть всё назад. Схема-миграции обычно создаются автоматически с помощью makemigrations и у них всегда есть зеркальные копии, в то время как дата-миграции пишут разработчики самостоятельно и лишний код они писать не хотят, поэтому зеркальных копий у дата-миграций часто нет. Более того, обратную миграцию написать не всегда возможно. Если вы пытаетесь отменить дата-миграцию, у которой нет "зеркальной" миграции, то она выдаст ошибку:

$ python manage.py migrate appname 0007
django.db.migrations.exceptions.IrreversibleError: Operation
<RunPython <function move_owners_to_authors at 0x7f4822c7e950>>
in appname.0008_auto_20190808_1314 is not reversible

Чтобы отменить дата-миграцию нужна вторая функция move_backward, которая вернёт всё назад. Она передаётся вторым аргументом в RunPython в самом конце файла:

from django.db import migrations


def move_owners_to_authors(apps, schema_editor):
    Post = apps.get_model('blog', 'Post')
    for post in Post.objects.all():
        post.author = post.owner
        post.owner = None
        post.save()


def move_backward(apps, schema_editor):
    Post = apps.get_model('blog', 'Post')
    for post in Post.objects.all():
        post.owner = post.author
        post.author = None
        post.save()


class Migration(migrations.Migration):

    dependencies = [
        ('appname', '0007_auto_20190808_1301'),
    ]

    operations = [
        migrations.RunPython(move_owners_to_authors, move_backward),
    ]

Повторный запуск дата-миграций

Обычно код дата-миграций подходит для многократного запуска, что очень помогает в отладке: дата-миграцию можно запускать раз за разом, пока та не заработает как надо. Но этому сопротивляется команда migrate — она не дает повторно запустить однажды выполненную миграцию. Чтобы "обмануть" migrate можно просто стереть из БД запись о запуске последней миграции:

$ python manage.py migrate --fake appname 0007
Rendering model states... DONE
  Unapplying appname.0008_auto_20190808_1314... FAKED

Опция --fake запускает миграций в "фейковом" режиме: удаляет из БД запись о том, что миграция 0008 раньше уже применялась, но пропускает запуск самого файла с миграцией 0008:

 [X] 0006_auto_20190807_1545
 [X] 0007_auto_20190808_1301
 [ ] 0008_auto_20190808_1314

Теперь вы можете переписать код миграции 0008 и запустить её заново:

$ python manage.py migrate
Running migrations:
  Applying appname.0008_auto_20190808_1314... DONE

$ python manage.py showmigrations
...
 [X] 0006_auto_20190807_1545
 [X] 0007_auto_20190808_1301
 [X] 0008_auto_20190808_1314

Чтобы этот прием эффективно работал миграция должна подходить для повторного запуска. Как этого добиться описано в статье про идемпотентные дата-миграции.

Подробнее о --fake

Благодаря --fake можно безнаказанно стирать из БД записи о миграциях. Но --fake опасен: если вы создали модель, затем отменили эту миграцию с помощью --fake, то запись о выполненной миграция пропадёт, но таблица в БД для этой модели останется. Схема база данных и миграции окажутся рассинхронизированными, и когда вы попытаетесь вновь создать таблицу, то получите ошибку: невозможно создать эту модель, так как она уже есть в базе данных.

Опция --fake чрезвычайно удобна при написании дата-миграций: если вам не понравилась какая-то дата-миграция, вы можете сделать вид, что её не было, переписать, и снова запустить. Это позволит не плодить сотни дата-миграций с исправлениями старой логики, а только поправлять один файл миграции и запускать его раз за разом.