Эта часть родилась больше из неожиданности, чем что-либо еще. Учитывая, насколько мы воспринимаем модульность как должное, я предположил, что это будет первоклассная концепция в чем-то столь же популярном, как Alembic (который, к слову, довольно звездный, и мне понравилось работать с ним).

Цель здесь — поделиться подходом к этой проблеме, который хорошо сработал для моей команды, и, надеюсь, сэкономит вам несколько ночей.

Цель

Отслеживайте миграцию приложений проекта отдельно, как концептуально, так и в перегонном кубе.

Подход

Если вы работали с последней версией Django, вы согласитесь, что миграция в ней выполняется довольно легко. В частности, подход с приложениями интуитивно понятен. Цель состояла в том, чтобы воспроизвести что-то подобное здесь. Предполагается, что каждое приложение имеет по крайней мере одну отдельную схему. Я еще не пробовал использовать кросс-приложение схемы, но нет никаких причин, по которым оно не должно работать. Наша команда использует скрипт dev в верхней части каталога проекта с оболочками для общих команд. В конце этого мы хотели бы иметь по крайней мере три команды;

  • ./dev makemigrations: генерировать миграции для последних изменений модели (по умолчанию мы будем автоматически генерировать миграции)
  • ./dev migrate <app_name>: применить сгенерированные миграции к базе данных
  • ./dev downgrade <app_name>: отменить последнюю примененную миграцию
  • Бонус- ./dev create <app_name>: устанавливает новое приложение

Покажи мне код

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

Настройка общих функций миграции

  • Добавьте раздел [alembic], в котором будет храниться конфигурация, которая будет использоваться, когда теперь --name не указывается при выполнении команд alembic. Мы будем использовать это для таких вещей, как проверка коллективных состояний головы. А пока просто добавьте это в этот раздел;
[alembic]
# universal config
script_location = alembic # dir where your common `env.py` file lives
  • Измените файл env.py, созданный перегонным кубом, на;
from logging.config import fileConfig
from typing import List

from sqlalchemy import engine_from_config, MetaData
from sqlalchemy import pool
from alembic import context

import settings

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers
fileConfig(config.config_file_name)

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def _include_object(target_schema):
    def include_object(obj, name, object_type, reflected, compare_to):
        if object_type == "table":
            return obj.schema in target_schema
        else:
            return True

    return include_object


def _run_migrations_offline(target_metadata, schema):
    """Run migrations in 'offline' mode.

    This configures the context with just a URL
    and not an Engine, though an Engine is acceptable
    here as well.  By skipping the Engine creation
    we don't even need a DBAPI to be available.

    Calls to context.execute() here emit the given string to the
    script output.

    """
    url = settings.CONNECTION_STRING
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
        include_schemas=True,  #1
        include_object=_include_object(schema),  #1
        compare_type=True,
    )

    with context.begin_transaction():
        context.run_migrations()


def _run_migrations_online(target_metadata, schema):
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
        url=settings.CONNECTION_STRING,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            include_schemas=True,  #2
            include_object=_include_object(schema),  #2
            compare_type=True,
        )

        with context.begin_transaction():
            context.run_migrations()

#3
def run_migrations(metadata: MetaData, schema: List[str]):
    if context.is_offline_mode():
        _run_migrations_offline(metadata, schema)
    else:
        _run_migrations_online(metadata, schema)

Ключевые изменения

1, 2.
В сгенерированном по умолчанию файле не установлен флаг include_schemas. Я столкнулся с трудностями при получении перегонного куба, чтобы найти изменения модели в схемах с выключенным флагом. Однако включение этого также привело к тому, что миграции в других схемах были ошибочно отнесены к той схеме, которую я пытался перенести в то время. Именно здесь в дело вступает аргумент include_object. Он принимает метод, который при вызове должен возвращать True или Falseв зависимости от того, должен ли он быть включен в сгенерированный файл миграции.

3.
Запуск миграции больше не выполняется при загрузке/выполнении файла, а при вызове этого метода. Схема позволяет нам выполнять упомянутую выше фильтрацию**.

Создание приложения

Надеюсь, к концу этого у нас будет команда (./dev create <app_name>) для автоматического выполнения этой шаблонной работы, но до тех пор добавьте эти шаги в свой README, потому что вам нужно будет выполнять их каждый раз, когда вы добавляете новое приложение.

  • Добавьте каталог миграции в свое приложение (условие здесь <app_name>/migrations, но не стесняйтесь следовать соглашению вашего проекта, например migrations/<app_name>).
  • Скопируйте файлscript.py.mako в этот каталог.***
  • Создайте файл env.py в этом каталоге и добавьте;
from alembic.env import run_migrations  # or wherever your common `env.py` file is
from <app_name>.models import Base

metadata = Base.metadata
schema = ["some_schema", "some_other_schema"]

run_migrations(metadata, schema)

Формат этого файла будет более или менее одинаковым для всех приложений с заменой соответствующей схемы и класса модели Base.

  • Добавьте раздел вашего приложения с соответствующими атрибутами в alembic.ini. Этого пока достаточно;
[your_app_name]
# <app_name> specific settings
script_location = <app_name>/migrations
  • Добавьте каталог переноса вашего приложения в alembic.ini/version_locations. Это позволит alembic отслеживать все места, где находятся ваши миграции, даже если вы запускаете alembic, используя другой раздел файла alembic.ini с другим расположением версий и другой версией. базы.
  • Создайте первую миграцию.
alembic \
    --name=<app_name> \
    revision -m "<message e.g Initial: Setup Models>" \
    --head=base \
    --branch-label=<app_name> \
    --version-path=<path to your app's migration files e.g <app_name>/migrations/versions> \
    [ --autogenerate]

Давайте разберем это.

  • --name позволяет нам выбрать раздел в конфигурационном файле для использования****.
  • --head=base предлагает запустить это дерево миграции в корне, что означает, что у него не будет предыдущих зависимостей. Это имеет смысл для нашего подхода к применению.
  • --branch-label будет именем этой ветки миграции. Прочтите о ветвях в документации, чтобы разобраться в этом.
  • --version-path говорит само за себя. Вероятно, вы хотите автоматически обнаруживать изменения модели, поэтому включите флаг --autogenerate.
  • Перенос.
    Запустите первый перенос этого приложения.alembic --name=<app_name> upgrade <app_name>@head.

Добавьте удобные команды в свой скрипт dev

Выполнив предыдущие шаги и настроив их, вы впоследствии будете просто запускать ./dev makemigrations, когда в ваших моделях появятся новые изменения, и ./dev migrate, чтобы применить их.

  • Добавьте команду ./dev makemigrations -m="Some message" <app_name>, которую вы будете выполнять для дальнейшей миграции*. Эта команда выполняется;
alembic \
    --name=<app_name> \
    revision \
    --message="Some message(your migrate command should probably take this in as an option)" \
    --head=<app_name>@head
    --autogenerate

*Для первой миграции в приложении вы запустите команду Создайте первую миграцию выше, а для последующих — эту.

  • Добавьте команду ./dev migrate <app_name>, которая запускает alembic --name=<app_name> upgrade <app_name>@head как показано выше.
  • Добавьте команду .dev downgrade <app_name>, которая запускает alembic --name<app_name> downgrade <app_name>@head-1.

Сноски

  • **: Если у вас есть одна схема для каждого приложения, вы можете даже исключить это и выбрать схему из файла metadata.
  • ***: Если вы найдете способ alembicиспользовать ранее сгенерированный файл env, в то же время указывая на местоположение скрипта, которое не является этим каталогом, дайте мне знать :)
  • ****: С этим можно делать интересные вещи. Например, я попробовал автоматическое обнаружение схемы и модулей на основе этого, но это не совсем хорошо закончилось. Это то, что может по-прежнему возможно, и может устранить необходимость в env файлах приложения.