Код-ревью Middle Миграции обновлено

Код-ревью миграции данных Django

Вопрос

Миграция заполняет новое поле slug у 3 млн статей. Найдите проблемы.

from django.db import migrations
from django.utils.text import slugify
from blog.models import Article


def fill(apps, schema_editor):
    for article in Article.objects.all():
        article.slug = slugify(article.title)
        article.save()


class Migration(migrations.Migration):
    dependencies = [('blog', '0012_article_slug')]
    operations = [
        migrations.RunPython(fill),
        migrations.AlterField(
            model_name='article',
            name='slug',
            field=models.SlugField(unique=True),
        ),
    ]

Коротко

Исправленный вариант делят на две миграции. Первая переносит данные: Вторая отдельно делает поле уникальным (а на большой таблице уникальный индекс лучше создать конкурентно).

Что должен найти ревьюер

  1. Ошибкастрока 3
    Импорт текущей модели вместо исторической: через полгода модель изменится, и миграция сломается; нужно apps.get_model("blog", "Article")
  2. Производительностьстрока 7
    Обход 3 млн объектов с кешированием QuerySet: память; нужен .iterator() и обработка пачками (bulk_update)
  3. Производительностьстрока 9
    save() без update_fields пишет все поля и шлёт сигналы на каждую строку; bulk_update(objs, ["slug"]) пачками
  4. Стильстрока 15
    Нет обратной функции: миграцию нельзя откатить; добавить migrations.RunPython.noop
  5. Ошибкастрока 16
    Перенос данных и смена схемы в одной миграции и одной транзакции: долгая транзакция и блокировки на всё время переноса
  6. Ошибкастрока 19
    unique=True упадёт на одинаковых заголовках (одинаковый slug), дубли нужно разрешить при заполнении; плюс models не импортирован

Разбор

Исправленный вариант делят на две миграции. Первая переносит данные:

def fill(apps, schema_editor):
    Article = apps.get_model('blog', 'Article')
    batch = []
    for a in Article.objects.filter(slug='').only('id', 'title').iterator(chunk_size=2000):
        a.slug = f'{slugify(a.title)}-{a.id}'
        batch.append(a)
        if len(batch) == 2000:
            Article.objects.bulk_update(batch, ['slug'])
            batch.clear()
    Article.objects.bulk_update(batch, ['slug'])

class Migration(migrations.Migration):
    atomic = False
    dependencies = [('blog', '0012_article_slug')]
    operations = [migrations.RunPython(fill, migrations.RunPython.noop)]

Вторая отдельно делает поле уникальным (а на большой таблице уникальный индекс лучше создать конкурентно). Суффикс id снимает проблему одинаковых заголовков.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.