Как запретить миграции Django с автоматическим именем

Когда вы запускаете команду Django manage.py makemigrations, она попытается сгенерировать имя для миграции на основе ее содержимого. Например, если вы добавляете одно поле, он назовет миграцию 0002_mymodel_myfield.py. Однако если миграция содержит более одного шага, вместо этого она использует простое имя 'auto' с текущей датой и временем, например, 0002_auto_20200113_1837.py. Вы можете указать аргумент -n/--name в makemigrations, но разработчики часто забывают об этом.

Именование вещей - известная сложная проблема в программировании. Наличие миграций с такими автоматическими именами усложняет управление ими: Вы не сможете определить, какая из них какая, не открыв их, и вы все равно можете перепутать их, если они имеют похожие имена из-за того, что были созданы в один и тот же день.

Это становится болезненным в случаях:

  • перебазирование веток
  • копаться в истории
  • развертывание в производство

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

Также очень легко забыть исправить имя и зафиксировать его, поскольку Django не подсказывает вам лучшее имя. Мы можем защититься от этого с помощью автоматизации!

Давайте рассмотрим три метода, позволяющие это сделать.

Обновление (2020-02-25): Первоначально эта статья включала только мою пользовательскую проверку системы (#2). Благодаря замечательным отзывам на Reddit и Twitter я включил еще два метода, оба из которых короче.

1. Переопределение makemigrations в require -n/--name

Обновление (2020-02-25): Спасибо @toyg на reddit за то, что указали на это.

В данном случае используется та же техника переопределения встроенной команды управления, которую я использовал в своем посте "Make Django Tests Always Rebuild the Database if It Exists".

Добавьте новую команду makemigrations в «основное» приложение вашего проекта (например, myapp/management/commands/makemigrations.py) со следующим содержимым:

from django.core.management.base import CommandError
from django.core.management.commands.makemigrations import Command as BaseCommand


class Command(BaseCommand):
    def handle(self, *app_labels, name, check_changes, dry_run, merge, **options):
        if name is None and not check_changes and not dry_run and not merge:
            raise CommandError(
                "Myproject customization: -n/--name is required."
            )
        super().handle(
            *app_labels,
            name=name,
            check_changes=check_changes,
            dry_run=dry_run,
            merge=merge,
            **options,
        )

(Замените "Myproject" на название вашего проекта.)

Нам не требуется имя, когда используются два флага: --check (переменная check_changes) или --dry-run (dry_run). Они не создают файлы миграции, поэтому требование имени будет утомительным.

После его создания, когда мы запустим makemigrations, мы увидим такое сообщение:

$ python manage.py makemigrations
Myproject customization: -n/--name is required.

Поскольку это изменение применяется только к makemigrations, оно автоматически влияет только на новые миграции, а не на миграции в сторонних приложениях. Приятно.

2. Пользовательская проверка системы

Обновление (2020-02-25): Благодаря Никите Соболеву, эта проверка доступна в пакете `django-test-migrations` начиная с версии 0.2.0+. Смотрите раздел "Тестирование имен миграций" в его документации.

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

Чтобы добавить его в свой проект, вам сначала нужно добавить его в модуль внутри одного из ваших приложений. Обычно я добавляю checks.py в "основное" приложение проекта (как бы оно ни называлось):

# myapp/checks.py
from fnmatch import fnmatch

from django.core.checks import Error


def check_migration_names(app_configs, **kwargs):
    from django.db.migrations.loader import MigrationLoader

    loader = MigrationLoader(None, ignore_no_migrations=True)
    loader.load_disk()

    errors = []
    for (app_label, migration_name), _ in loader.disk_migrations.items():
        if (app_label, migration_name) in IGNORED_BADLY_NAMED_MIGRATIONS:
            continue
        elif fnmatch(migration_name, "????_auto_*"):
            errors.append(
                Error(
                    f"Migration {app_label}.{migration_name} has an automatic name.",
                    hint=(
                        "Rename the migration to describe its contents, or if "
                        + "it's from a third party app, add to "
                        + "IGNORED_BADLY_NAMED_MIGRATIONS"
                    ),
                    id="myapp.E001",
                )
            )

    return errors


IGNORED_BADLY_NAMED_MIGRATIONS = {
    # Use to ignore pre-existing auto-named migrations:
    # ('myapp', '0002_auto_20200123_1257'),
}

Некоторые замечания по коду:

  • Мы должны использовать внутренний импорт для MigrationLoader, поскольку он зависит от загрузки всех приложений Django, а мы импортируем нашу проверку до этого.
  • Мы говорим загрузчику миграций загрузить имена всех миграций с диска и перебрать их.
  • Мы используем стандартную библиотеку fnmatch функцию для выполнения простого сопоставления строк с именем файла. Это легче читать и писать, чем использовать регулярные выражения.
  • Внизу у нас есть IGNORED_BADLY_NAMED_MIGRATIONS, набор из двух кортежей типа (имя приложения, имя миграции). Я оставил прокомментированный пример ожидаемой структуры данных.

Чтобы запустить проверку, нам нужно зарегистрировать ее в AppConfig.ready() нашего приложения:

# myapp/apps.py
from django.apps import AppConfig
from django.core import checks

from myapp.checks import check_migration_names


class MyappConfig(AppConfig):
    name = 'myapp'

    def ready(self):
        checks.register(checks.Tags.compatibility)(check_migration_names)

… И убедитесь, что мы используем наш AppConfig в INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    'myapp.apps.MyappConfig',
    # ...
]

Запуск проверок выявит все проблемные файлы миграции:

$ python manage.py check
SystemCheckError: System check identified some issues:

ERRORS:
?: (myapp.E001) Migration myapp.0002_auto_20200123_1257 has an automatic name.
    HINT: Rename the migration to describe its contents, or if it's from a third party app, add to IGNORED_BADLY_NAMED_MIGRATIONS

System check identified 1 issue (0 silenced).

Django also runs checks at the start of most manage.py commands, and in the test runner.

Если вы добавляете это в проект с уже существующими автоименованными миграциями, каждая из них будет отображаться как ошибка. Вам следует добавить их в IGNORED_BADLY_NAMED_MIGRATIONS, а не переименовывать их. Django знает миграции только по имени, поэтому если вы переименуете их, он определит их как непримененные и попытается применить их снова - упс.

3. С хуком a pre-commit

Обновление (2020-02-25): Энтони Соттиле, создатель pre-commit, указал на эту более короткую технику в Twitter.

Если вы используете pre-commit (а вы должны использовать, он действительно хорош!), вы также можете использовать хук для запрета автоматически создаваемых файлов с гораздо меньшим количеством кода:

- repo: local
  hooks:
  - id: no-auto-migrations
    name: no auto-named migrations
    entry: please provide a descriptive name for migrations
    language: fail
    files: .*/migrations/.*_auto_.*\.py$
    exclude: ^
      (?x)^(
        myapp/migrations/0002_auto_20200123_1257\.py
        |myapp/migrations/0003_auto_20200123_1621\.py
      )$

Это использует псевдоязык ошибки для автоматического сбоя любых файлов, соответствующих этому регулярному выражению. Довольно аккуратно!

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

https://adamj.eu/tech/2020/02/24/how-to-disallow-auto-named-django-migrations/

Вернуться на верх