В каких случаях нужны кастомные миграции в Django и как их создать

Ответ

Кастомные миграции в Django необходимы, когда стандартных операций makemigrations (изменение схемы БД) недостаточно для обновления состояния базы данных.

Когда это необходимо:

  • Миграция данных (Data Migration): Заполнение новых полей на основе значений существующих, преобразование формата данных в колонке.
  • Выполнение сырых SQL-запросов: Создание триггеров, хранимых процедур или использование специфичных для СУБД функций, которые не поддерживаются Django ORM.
  • Зависимости от внешних систем: Например, если нужно синхронизировать данные с внешним API после изменения схемы.
  • Сложные переименования или реструктуризация: Когда простое переименование поля в модели может привести к потере данных.

Как создать и использовать:

  1. Создать пустой файл миграции:
    python manage.py makemigrations --empty yourappname
  2. Написать код миграции: В сгенерированном файле используются операции migrations.RunPython для выполнения Python-кода или migrations.RunSQL для SQL-запросов.

Пример: migrations.RunPython для преобразования данных

Предположим, мы хотим объединить поля first_name и last_name в новое поле full_name.

Файл миграции 0002_populate_full_name.py:

from django.db import migrations

def combine_names(apps, schema_editor):
    # Получаем историческую версию модели, чтобы миграция не зависела от текущего кода
    User = apps.get_model('auth', 'User')
    for user in User.objects.all():
        user.full_name = f"{user.first_name} {user.last_name}".strip()
        user.save()

# Функция для отката миграции (опционально, но рекомендуется)
def uncombine_names(apps, schema_editor):
    User = apps.get_model('auth', 'User')
    User.objects.update(full_name="")

class Migration(migrations.Migration):

    dependencies = [
        ('auth', '0001_initial'), # Зависит от предыдущей миграции
    ]

    operations = [
        migrations.RunPython(combine_names, reverse_code=uncombine_names),
    ]

Ключевые моменты:

  • apps.get_model(): Всегда используйте этот метод вместо прямого импорта модели, чтобы работать с её версией на момент применения миграции.
  • reverse_code: Предоставляйте функцию для отката изменений, чтобы миграцию можно было отменить.
  • Производительность: Для больших объемов данных используйте queryset.iterator() или queryset.bulk_update() для экономии памяти.

Ответ 18+ 🔞

А, ну вот, смотри, классика жанра! Кастомные миграции в Django — это когда стандартный makemigrations смотрит на твою модель, пожимает плечами и говорит: «Ну, тут, блядь, я пас, разбирайся сам».

Когда это, сука, нужно:

  • Миграция данных (Data Migration): Ну, например, ты добавил поле full_name, а в старых записях там пусто. Надо взять first_name и last_name, склеить их и запихнуть на место. Или, допустим, ты раньше хранил телефон как строку, а теперь хочешь его чистить и валидировать — надо пройтись по всей базе и поправить.
  • Сырой SQL: Когда тебе надо нагородить какую-нибудь хранимую процедуру, триггер или использовать фичу базы данных, про которую Django ORM даже не слышал. ORM в такие моменты делает вид, что не понимает, о чём ты.
  • Зависимости от внешнего мира: Скажем, после добавления поля нужно пойти нахуй к какому-нибудь API, дёрнуть оттуда данные и заполнить это поле. Стандартные миграции на такое не способны, им бы свою схему поправить.
  • Сложные переезды: Просто переименовать поле — легко. А если тебе надо разбить одну таблицу на две, перенести часть данных, при этом не потерять связи? Вот тут и начинается настоящий цирк, блядь.

Как это, сука, делается:

  1. Создаёшь пустой файл для творчества:

    python manage.py makemigrations --empty yourappname

    Django тебе любезно накидает заготовочку, а дальше — твой выход.

  2. Пишешь код, который всё и поломает (или починит): В этом файле тебе доступны две главные пушки: migrations.RunPython (чтобы запустить твой питонячий код) и migrations.RunSQL (чтобы впендюрить в базу чистый SQL, от которого у ORM случится инфаркт).

Пример на живца: migrations.RunPython

Допустим, у нас есть модель User от django.contrib.auth. Мы добавили поле full_name, и надо его заполнить из first_name и last_name.

Вот так выглядит файл миграции 0002_populate_full_name.py:

from django.db import migrations

def combine_names(apps, schema_editor):
    # ВАЖНО! Не импортируй модель напрямую! Берём её историческую версию.
    # Иначе через полгода, когда модель изменится, миграция ебнется.
    User = apps.get_model('auth', 'User')
    for user in User.objects.all():
        user.full_name = f"{user.first_name} {user.last_name}".strip()
        user.save()

# Функция для отката. Если что-то пойдёт не так, сможем откатиться.
def uncombine_names(apps, schema_editor):
    User = apps.get_model('auth', 'User')
    User.objects.update(full_name="")

class Migration(migrations.Migration):

    dependencies = [
        ('auth', '0001_initial'), # Цепляемся к предыдущей миграции
    ]

    operations = [
        migrations.RunPython(combine_names, reverse_code=uncombine_names),
    ]

На что смотреть, чтобы не обосраться:

  • apps.get_model(): Это святое, блядь. Всегда используй только этот метод. Иначе твоя миграция станет хрупкой, как яичко фаберже, и сломается при первом же изменении модели.
  • reverse_code: Старайся писать функцию для отката. Это как страховочная верёвка. Без неё откатить миграцию будет сложно, а иногда и невозможно. Не будь мудаком, как тот Герасим из рассказа — предусматривай обратный ход.
  • Производительность: Если у тебя в таблице не десять записей, а десять тысяч, то цикл for user in User.objects.all() спасёт тебе всю оперативку. Используй iterator() или bulk_update(), чтобы не превратить сервер в кирпич. Иначе админ базы данных придёт и лично впендюрит тебе этот самый iterator() куда подальше.

Вот и вся магия. Главное — не бояться и помнить, что кастомная миграция это последнее средство, когда ORM уже бессилен. А то некоторые так увлекаются, что начинают всю бизнес-логику в миграции пихать, а потом удивляются, почему ничего не работает.