Как запретить миграции 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/
Вернуться на верх