Оригинальная статья: Adam Johnson — How to Add Database Modifications Beyond Migrations to Your Django Project
В нескольких проектах Django, над которыми я работал, была необходимость в автоматическом внесение изменений в базу данных после проведенных миграций. Такая необходимость может возникнуть например для:
Давайте рассмотрим три возможных способа, сделать это максимально аккуратно.
Часто я нахожу, что разработчиков учат только тому, как использовать миграции Django для операций с моделями. Они могут знать некоторые SQL команды, но, поскольку они не используют их в Django, они предполагают, что миграции не могут напрямую использовать SQL команды. Но они могут! Многие из применений пользовательского кода SQL в стиле миграции, которые я видел, могли бы быть лучше реализованы в рамках миграций.
Давайте посмотрим на пример, который мы будем использовать для остальной части статьи. Представьте, что вы используете версию Django до 2.2, которая не поддерживает database check constraints. Вы можете добавить такое ограничение в таблицу базы данных используя следующий SQL:
ALTER TABLE myapp_book ADD CONSTRAINT percent_lovers_haters_sum CHECK ( (percent_lovers + percent_haters) = 100 );
Это ограничение заставит базу данных выдавать ошибку для любых строк, добавленных или обновленных в таблице myapp_book, у которых сумма percent_lovers и percent_haters не равна 100.
При использовании именования таблиц по умолчанию в Django таблица myapp_book будет создана для модели под названием Book внутри приложения myapp. Мы будем использовать эти имена и в этой статье.
Вышеприведенный SQL может быть выполнен с использованием django.db.connection:
from django.db import connection with connection.cursor() as cursor: cursor.execute(""" ALTER TABLE myapp_book ADD CONSTRAINT percent_lovers_haters_sum CHECK ( (percent_lovers + percent_haters) = 100 ); """)
Это работает, но это немного не типично. Вы можете запустить его с помощью manage.py shell или лучше в custom management (пользовательской команде управления) (подробнее об этом позже!).
Или вместо этого мы можем использовать миграцию.
Во-первых, вы можете создать новую миграцию с помощью:
$ python manage.py makemigrations --empty myapp Migrations for 'myapp': myapp/migrations/0101_auto_20190715_1057.py
Затем отредактируем этот файл миграции с использованием операции RunSQL:
from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('myapp', '0100_add_book'), ] operations = [ migrations.RunSQL(""" ALTER TABLE myapp_book ADD CONSTRAINT percent_lovers_haters_sum CHECK ( (percent_lovers + percent_haters) = 100 ); """) ]
Еще желательно переименовать файл миграции во что-то описательное. Например, вместо 0101_auto_20190715_1057.py мы могли бы переименовать в 0101_add_book_percentage_sum_constraint.py. Это очень поможет в долгосрочной перспективе.
Чтобы еще улучшить миграцию, мы можем добавить аргумент reverse_sql. Он говорит Django, как обратить вспять процесс миграции. Вам редко понадобится отменять миграцию, но когда вам это понадобиться… он реально спасет положение!
В нашем примере мы можем расширить операцию миграции до:
migrations.RunSQL( sql=""" ALTER TABLE myapp_book ADD CONSTRAINT percent_lovers_haters_sum CHECK ( (percent_lovers + percent_haters) = 100 ); """, reverse_sql="ALTER TABLE myapp_book DROP CONSTRAINT percent_lovers_haters_sum", )
Это удалит проверочное ограничение при отмене операции.
Если код, который вы хотите выполнить, является более сложным, например, если вы хотите выполнить что-то для каждой модели/таблицы, вы можете использовать операцию RunPython. С его помощью вы можете делать практически все что угодно!
Этот подход подходит для многих сценариев использования. Так же, мы предусматриваем возможность отмены наших изменений.
Недостатком является то, что этот способ сработает только один раз. Он может быть бесполезным, если у вас есть что-то, что нужно часто запускать, к примеру вам может потребоваться добавить много операций.
Вы можете написать собственную операцию миграции или подключиться к сигналу pre_migrate
signal. Однако, по моему опыту, следующие два подхода будут проще.
Это похоже на секретную функцию, учитывая, как мало проектов, которые я видел, используют ее. Тем не менее, я нашел его удобным в нескольких случаях.
Django позволяет вам переопределить команды управления, добавив еще одну с тем же именем. Вы можете переопределить команды из ядра Django или из других приложений. Это задокументировано в «пользовательских командах управления».
При запуске команды Django просматривает все приложения в INSTALLED_APPS, а затем в ядре. Первым делом он найдет пользовательскую команду управления с заданным именем. Таким образом, ваши приложения могут переопределять любые встроенные модули.
Чтобы переопределить migrate и добавить собственное поведение, вам нужно создать myapp/management/commands/migrate.py (заменив myapp именем одного из ваших приложений). Внутри этого файла вы можете создать подкласс встроенной команды migrate и добавить собственное поведение. Например:
from django.core.management.commands.migrate import Command as CoreMigrateCommand from myapp.db import create_constraints class Command(CoreMigrateCommand): def handle(self, *args, **options): # Do normal migrate super().handle(*args, **options) # Then our custom extras create_constraints()
(Документация по пользовательским командам управления, может быть очень полезной в данном случае.)
Переопределение работает, потому что во всем коде Django миграция вызывается как команда, а не импортируется напрямую. Это делается с помощью функции call_command. Таким образом, если вы переопределили migrate, вызовется новая версия вместо основной.
Мы можем увидеть это в тестовом фреймворковом коде Django. Его функция setup_databases вызывает метод каждого соединения create_test_db. Они, в свою очередь, запускают call_command(‘migrate’) следующим образом:
# We report migrate messages at one level lower than that requested. # This ensures we don't get flooded with messages during testing # (unless you really ask to be flooded). call_command( 'migrate', verbosity=max(verbosity - 1, 0), interactive=False, database=self.connection.alias, run_syncdb=True, )
Вы можете переопределить любую встроенную команду.
Этот подход более гибкий, чем использование RunSQL в миграциях. Мы можем добавить любой код, который мы хотим, до или после выполнения migrate, или даже «во время» с помощью диспетчера контекста.
Основным недостатком данного подхода является то, что мы можем переопределить только один раз, в одном приложении, поэтому может показаться немного неуклюжим, если у нас есть несколько расширений для конкретного приложения. Однако для большинства проектов я бы порекомендовал именно это способ.
Наличие одного «проектного приложения» вполне рабочий выбор — я поддерживаю рекомендацию из неофициального Kristian Glass’ Unofficial FAQ. Если у вас уже есть несколько приложений, вы можете сделать одно из них «основным», и пусть оно будет содержать пользовательскую команду migrate, а импортировать код из других приложений. Это будет просто отличный подход.
Тем не менее, иногда мы хотим создавать более слабые связи, например, при создании сторонних пакетов. Итак, давайте посмотрим на окончательный подход.
Этот подход еще более продвинутый. Он использует сигналы Django, которые имеют неоднозначную репутацию из-за их «действия на расстоянии» (action at a distance).
Django отправляет сигнал post_migrate в самом конце операций миграции. Вы можете увидеть это в исходном коде migrate
.
Чтобы запустить какой-то дополнительный код на этом этапе, запишите его как обработчик сигнала. Зарегистрировать обработчика сигнала лучше всего в методе AppConfig.ready(), который Django будет вызывать во время инициализации.
Например, давайте посмотрим на Django contenttypes framework. Он импортируется из django.contrib.contenttypes. И использует обработчик сигнала post_migrate для создания одного экземпляра ContentType для каждой модели.
Обработчик create_contenttypes зарегистрирован в AppConfig.ready() примерно так:
class ContentTypesConfig(AppConfig): name = 'django.contrib.contenttypes' verbose_name = _("Content Types") def ready(self): pre_migrate.connect(inject_rename_contenttypes_operations, sender=self) post_migrate.connect(create_contenttypes) checks.register(check_generic_foreign_keys, checks.Tags.models) checks.register(check_model_name_lengths, checks.Tags.models)
Обработчик определен в management/__init__.py. Это не самое описательное имя файла, содержащее обработчик сигнала — я обычно использую handlers.py в app. У фреймворка contenttypes есть это по историческим причинам.
В Django 2.2.3 create_contenttypes определяется следующим образом:
def create_contenttypes(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs): """ Create content types for models in the given app. """ if not app_config.models_module: return app_label = app_config.label try: app_config = apps.get_app_config(app_label) ContentType = apps.get_model('contenttypes', 'ContentType') except LookupError: return content_types, app_models = get_contenttypes_and_models(app_config, using, ContentType) if not app_models: return cts = [ ContentType( app_label=app_label, model=model_name, ) for (model_name, model) in app_models.items() if model_name not in content_types ] ContentType.objects.using(using).bulk_create(cts) if verbosity >= 2: for ct in cts: print("Adding content type '%s | %s'" % (ct.app_label, ct.model))
Здесь довольно много логики. Но мы можем проигнорировать большую часть, так как он зависит от конкретного случая использования.
Интересно посмотреть на аргументы функции.
Обработчики сигналов вызываются только с ключевыми словами. Для прямой совместимости они должны принимать любые дополнения в конце в **kwargs, чтобы нераспознанные аргументы, добавленные отправителем, не нарушали обработчик — таким образом создается более слабая связь.
Все аргументы, перечисленные здесь, соответствуют документации post_migrate:
Для написания большинства пользовательских обработчиков, я думаю, что два аргумента являются наиболее полезными.
Во-первых, app_config может использоваться для ограничения вашего обработчика на запуск только для определенного приложения или набора приложений. Вы также можете сделать это с помощью аргумента отправителя для Signal.connect().
Во-вторых, стоит всегда описывать using для любых операции с базой данных, которые вы используете. Даже если ваш проект в настоящее время использует одну базу данных, в будущем это может измениться.
Как мы уже говорили, преимуществом этого подхода является более слабая связь. Если вы пишете сторонний пакет, это, будет более, правильным путем, поскольку он уменьшает количество зависимостей, которые необходимо установить пользователям.
Я надеюсь, что эта статья поможет вам найти правильный подход для вашего проекта при выборе способа автоматического внесения изменения после проведения миграций.
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…