Эта часть родилась больше из неожиданности, чем что-либо еще. Учитывая, насколько мы воспринимаем модульность как должное, я предположил, что это будет первоклассная концепция в чем-то столь же популярном, как 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
файлах приложения.