Как написать дата-миграцию

В этой статье мы поговорим об особых миграциях. Если вы не знаете, что такое миграции — вот статья о них.

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

Хочешь менять данные — пиши миграцию

Сразу начнём с примера: у вас есть интернет-магазин, на котором одни пользователи могут размещать объявления о продаже товаров, а другие могут эти товары покупать. Вы написали две модели: покупателя и продавца:

class Customer(models.Model):
    name = models.CharField(max_length=200)
    ...

class Seller(models.Model):
    name = models.CharField(max_length=200)
    ...

В ходе активной разработки сайта вы вдруг осознали: продавцы тоже могут быть покупателями, они тоже могут что-нибудь купить. Стало ясно, что нужна модель User, куда вы соберёте все данные из Customer и Seller и затем удалите эти старые модели из БД.

Обычная миграция здесь не поможет. Максимум что вы сможете — это переименовать одну из моделей в User. Но как перетащить данные из двух моделей в одну?.. Есть вариант с shell-скриптом: создать модель User, потом написать скрипт, который перенесёт всех пользователей в новую модель. Но у этого подхода есть два существенных минуса:

  • Без инструкции тут не разобраться. У вас есть 2 миграции: одна создаёт модель User, другая удаляет Customer и Seller. shell-скрипт должен работать строго между ними. Если случайно запустить вторую миграцию до shell-скрипта, то данные удалятся безвозвратно.
  • Всем разработчикам придётся повторять это шаманство у себя на компьютерах.

Вместо этого можно воспользоваться дата-миграциями и сделать всё абсолютно безопасно, контролируемо и в пару команд. А потом просто запустить migrate на сервере.

Для начала создайте модель User:

class User(models.Model):
    name = models.CharField(max_length=200)
    ...

class Customer(models.Model):
    name = models.CharField(max_length=200)
    ...

class Seller(models.Model):
    name = models.CharField(max_length=200)
    ...

Теперь создайте пустую миграцию. Вместо appname укажите имя приложения, т.е. название папки, где лежит models.py, который вы собираетесь менять:

python manage.py makemigrations --empty appname

Создастся миграция примерно с таким текстом:

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('appname', '0009_auto_20170602_1756'),
    ]

    operations = [
    ]

Далее добавьте в operations операцию RunPython. Она делает одну простую вещь: запускает функцию, которую вы ей передадите. Теперь вам нужны две функции: одна переместит в новую модель продавцов, а другая — покупателей:

    operations = [
        migrations.RunPython(copy_customers_to_users),
        migrations.RunPython(copy_sellers_to_users),
    ]

Осталось только написать эти функции. Вот пример одной из них:

def copy_customers_to_users(apps, schema_editor):
    User = apps.get_model('appname', 'User')
    Customer = apps.get_model('appname', 'Customer')
    for customer in Customer.objects.all():
        User.objects.get_or_create(name=customer.name)
        ...

Вместо обычного create здесь вы используете get_or_create. Он тоже создает записи в таблице, но делает это иначе. Отлаживая код вы будете запускать миграцию повторно много раз, и метод get_or_create не даст замусорить таблицу Customer множеством дублей. Подробнее о том как это работает читайте в статье про идемпотентные дата-миграции и в документации к методу get_or_create.

Обратите внимание, что модели вы не просто импортируете, а получаете с помощью get_model. Это потому, что код миграций должен запускаться вне зависимости от того, что лежит в models.py. Следующей миграцией вы удалите модели Customer и Seller из файла models.py, импортировать их оттуда станет невозможно, а дата-миграция обязана работать всё так же исправно.

Никогда не импортируйте из models.py

После написания функций copy_customers_to_users и copy_sellers_to_users достаточно запустить migrate, как для обычной миграции, и ваши данные скопируются в новую модель:

$ python manage.py migrate
...
0010_create_user... OK
0011_copy_customers_and_sellers_to_user... OK
0012_remove_customers_and_sellers... OK

Что почитать