Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Alembic: миграции БД на практике

Alembic на практике: ревизии, autogenerate и безопасные миграции

Рабочий процесс Alembic: инициализация и конфиг, autogenerate и его ограничения, ручные правки ревизий, data-миграции, безопасные ALTER на проде, даунгрейды — без страха миграций.

Прямой ответ

Рабочий цикл Alembic: изменил модели → alembic revision --autogenerate -m "описание" → проверил и дочистил сгенерированную ревизию → alembic upgrade head локально → ревизия в git вместе с моделями. Ключевые принципы: autogenerate — черновик, а не истина (он не видит изменения колонок по умолчанию, переименования трактует как drop+add, не умеет data-миграции); миграция — код, который ревьюится; на больших прод-таблицах «простой» ALTER может заблокировать таблицу — тяжёлые изменения раскладываются на безопасные шаги (добавить nullable → заполнить → переключить). И даунгрейды пишутся сразу: миграция без downgrade — технический долг в момент аварии.

Инициализация и конфигурация

alembic init migrations создаёт каркас: alembic.ini (URL БД — правильнее читать из env, не хардкодить), migrations/env.py (точка интеграции: сюда передаётся Base.metadata ваших моделей — без этого autogenerate не работает), versions/ (ревизии). Настройка env.py под проект: URL из переменной окружения (секреты — по правилам env), target_metadata = Base.metadata, для PostgreSQL — compare_type=True (иначе смена типа колонки не детектируется) и include_schemas при нескольких схемах. Локальная БД — docker-сервис dev-стека (настройка PostgreSQL); таблица alembic_version в БД отслеживает текущую ревизию — одна на базу.

Autogenerate: что он видит и что нет

Хорошо детектируется: новые/удалённые таблицы и колонки, индексы (с оговорками), ограничения. Не детектируется/трактует неверно: переименования (столбца/таблицы → drop+add с потерей данных — правьте руками на op.alter_column/rename), изменения типов (без compare_type), server_default, изменения в CHECK-констрейнтах частично, порядок колонок. Отсюда правило: сгенерированную ревизию читают как код — каждый op сверяется с намерением; «доверился автогену и накатил» — источник потерянных колонок. Частая ловушка — autogenerate хочет удалить то, чего нет в метаданных (таблицы других приложений в общей БД): ограничивайте include_object/version_table_schema своим контуром.

Ревизии как код: структура и data-миграции

Ревизия — Python-файл с upgrade()/downgrade(): DDL-операции (op.add_column, op.create_index) и, при необходимости, DML (op.execute("UPDATE ...") — data-миграция). Практики: схемная и data-миграция — разными ревизиями (разная цена отката); batch-режим для SQLite (op.batch_alter_table — SQLite не умеет большинство ALTER; почему это важно даже когда прод — PostgreSQL — локальные тесты на SQLite ломаются иначе); идемпотентность не нужна (Alembic гарантирует однократность), но атомарность важна — в PostgreSQL DDL транзакционен, и миграция целиком откатится при ошибке (в MySQL — нет, там осторожнее). Несколько голов после слияния веток — alembic merge heads (создаёт объединяющую ревизию) — штатная операция, не катастрофа.

Безопасные миграции на больших таблицах

Прямой ALTER TABLE ... ADD COLUMN ... NOT NULL DEFAULT на таблице в сотни миллионов строк — операция с последствиями (в современных PostgreSQL добавление с константным default быстро, но rewrite-операции и валидация констрейнтов блокируют запись). Паттерн expand-contract: expand — добавить nullable-колонку/новый индекс (CONCURRENTLY в PostgreSQL — без блокировки, но вне транзакции — autocommit_block() в Alembic); migrate — заполнить данные батчами в фоне; contract — переключить код, затем ужесточить (SET NOT NULL через валидацию констрейнтом) и удалить старое отдельной миграцией. Индексы — только CONCURRENTLY на живых таблицах; дропы колонок — отдельным релизом после того, как код перестал их читать.

Процесс в команде и CI

Миграции живут в git рядом с кодом моделей (ревизия без моделей в том же коммите — рассинхрон веток); в CI — прогон upgrade head на чистой тестовой БД (ловит битые ревизии до прода; настройка матрицы — в статье про GitHub Actions); на деплое — alembic upgrade head до старта новых инстансов приложения (обратный порядок — окно, когда новый код работает со старой схемой). alembic history/current — диагностика состояния; alembic downgrade -1 — шаг назад (поэтому downgrade пишется сразу и проверяется локально). Связь с ORM: модели — источник ожидаемой схемы, autogenerate — детектор расхождений; create_all в проде не используется (почему — в статье про сессии и схему SQLAlchemy).

Практический сценарий: добавление колонки в живую таблицу

Разберём самую частую операцию целиком — добавление колонки status в таблицу orders на сто миллионов строк. Шаг первый: модель меняется (status: Mapped[str] = mapped_column(String(20), nullable=True)), генерируется ревизия с единственным op.add_column — nullable, без default, без NOT NULL; накатывается она мгновенно, потому что перезаписи таблицы не происходит. Шаг второй: код приложения начинает писать значение в новую колонку при создании и обновлении записей, но читает пока старую логику — деплой этого кода ничего не ломает ни до, ни после миграции. Шаг третий: фоновая задача батчами заполняет status для исторических строк (по десять тысяч за транзакцию, с паузами — непрерывный UPDATE на миллионы строк держит блокировки и раздувает WAL). Шаг четвёртый: отдельная ревизия добавляет CHECK-констрейнт или NOT NULL через валидацию — в PostgreSQL это ALTER TABLE ... VALIDATE CONSTRAINT отдельной командой, которая не блокирует запись. Шаг пятый: код переключается на чтение новой колонки, и только следующим релизом старое поле помечается deprecated. Каждый шаг здесь — отдельная ревизия и отдельный деплой, и в любой момент можно остановиться или откатиться на шаг назад без потери данных. Именно эта «скучная» многоступенчатость отличает миграции, которые проходят незамеченно, от «одного простого ALTER», после которого сервис лежит двадцать минут.

FAQ

Можно ли править применённую ревизию? Нет: у задеплоенных ревизий — только новые ревизии поверх. Править файл имеет смысл до первого применения (в своей ветке); после — alembic stamp и корректирующая миграция, и это уже хирургия.

Что делать, если миграция упала на проде на середине? В PostgreSQL транзакционная DDL откатит её целиком (состояние — «до»); разбираете причину, чините ревизию новой. В MySQL частичное применение — ручная сверка alembic current с фактической схемой и stamp. Отсюда — прогон миграций на копии прода перед релизом.

Нужны ли даунгрейды всегда? Для схемных изменений — да (откат релиза без них невозможен). Для data-миграций честный downgrade часто невозможен (удалённые данные не вернуть) — фиксируйте это комментарием и делайте бэкап перед накатом.

Alembic без SQLAlchemy? Возможен (чистые op.execute/текстовый SQL), но основная ценность — связь с метаданными моделей. Для не-SQLAlchemy-проектов часто проще SQL-миграции (sqldef/просто нумерованные .sql), Alembic там — лишняя надстройка.

Источники: документация Alembic (tutorial, autogenerate), PostgreSQL ALTER TABLE — ссылки в шапке.