Миграции схемы БД
| Назначение | воспроизводимое изменение схемы БД по версиям |
|---|---|
| Хранятся | в репозитории рядом с кодом |
| Подходы | версионный (императивный), декларативный (сравнение состояний) |
| Python | Alembic, Django migrations |
| JVM и универсальные | Flyway, Liquibase |
| Go и другие | golang-migrate, goose, Atlas, Prisma Migrate, sqlx migrate |
Миграции схемы базы данных — упорядоченный набор версионируемых скриптов, каждый из которых переводит структуру базы из одного состояния в следующее: создаёт таблицы, добавляет столбцы и индексы, переименовывает поля, переносит данные. Миграции хранятся в системе контроля версий вместе с кодом приложения, а специальный инструмент знает, какие из них уже применены к конкретной базе, и применяет недостающие. Так схема на ноутбуке разработчика, в тестах и в продакшене остаётся одинаковой и воспроизводимой.[6]
Зачем нужны
Без миграций изменения схемы делаются вручную через консоль: кто-то забывает добавить столбец на стенде, порядок изменений теряется, новую базу невозможно поднять с нуля. Миграции решают это:
- история изменений схемы видна в репозитории и проходит ревью, как код;
- новая среда поднимается одной командой;
- изменения схемы выкатываются автоматически в пайплайне CI/CD вместе с приложением;
- можно откатить изменение (если миграция обратима).
Как работают
Инструмент создаёт в базе служебную таблицу (alembic_version, django_migrations, flyway_schema_history) с перечнем применённых миграций. Команда «обновить» сравнивает её со списком файлов и выполняет недостающие по порядку, обычно каждую в своей транзакции.
Два подхода:
- Версионный (императивный) — разработчик пишет (или генерирует) последовательность шагов:
V1__create_users.sql,V2__add_email.sql. Так работают Flyway, Alembic, Django, golang-migrate.[3] - Декларативный — описывается желаемое состояние схемы, а инструмент сам вычисляет разницу с текущей базой и строит план изменений: Atlas, Prisma (
db push), sqldef.[5] Удобно, но план нужно внимательно проверять — особенно удаления.
Примеры
Alembic (для SQLAlchemy) умеет автоматически генерировать миграцию по изменениям моделей ORM:[1]
alembic revision --autogenerate -m "add user email"
alembic upgrade head # применить все
alembic downgrade -1 # откатить последнюю
def upgrade() -> None:
op.add_column("users", sa.Column("email", sa.String(255), nullable=True))
op.create_index("ix_users_email", "users", ["email"], unique=True)
def downgrade() -> None:
op.drop_index("ix_users_email", table_name="users")
op.drop_column("users", "email")
Django делает то же встроенными командами makemigrations и migrate (см. Django).[2] Flyway и Liquibase работают с чистыми SQL-файлами или описаниями в XML/YAML и популярны в мире JVM, но подходят для любого стека.[4]
Автогенерацию всегда нужно читать глазами: инструмент может принять переименование столбца за удаление и создание нового — с потерей данных.
Изменения без простоя
В продакшене база работает под нагрузкой, а приложение выкатывается постепенно: какое-то время старая и новая версии кода работают одновременно. Отсюда два правила.
1. Блокировки. Многие операции ALTER TABLE берут эксклюзивную блокировку и на большой таблице «подвешивают» все запросы.[7] В PostgreSQL:
- индексы создавать с
CREATE INDEX CONCURRENTLY(вне транзакции); - новые столбцы добавлять без
DEFAULT, вычисляемого для каждой строки, или с константным значением по умолчанию (с PG 11 это мгновенно); NOT NULLи внешние ключи добавлять какNOT VALID, а затем проверять отдельноVALIDATE CONSTRAINT;- задавать
SET lock_timeout = '5s', чтобы миграция не встала в очередь за долгой транзакцией и не заблокировала всё за собой.
В MySQL для больших таблиц используют онлайн-DDL или инструменты gh-ost и pt-online-schema-change.
2. Совместимость версий: «расширить и сжать» (expand and contract). Опасные изменения разбивают на несколько выкладок. Например, переименование столбца name → full_name:
- Расширить: добавить столбец
full_name; код пишет в оба столбца. - Перенести данные: заполнить
full_nameдля старых строк пачками, чтобы не держать долгую транзакцию. - Переключить: код читает только
full_name. - Сжать: после того как старые версии кода выведены из работы, удалить
name.
Каждый шаг совместим с соседними версиями приложения, и любой можно откатить.
Практики
- Одна миграция — одно логическое изменение; уже применённые в общих средах миграции не редактируются, ошибки исправляются новой миграцией.
- Миграции данных отделяют от миграций схемы и делают идемпотентными.
- В CI проверяют, что миграции применяются на чистой базе и что модели совпадают со схемой (
alembic check,makemigrations --check); линтеры вроде squawk предупреждают об опасных операциях. - Перед рискованной миграцией — свежий бэкап и проверенное восстановление (см. Бэкапы: правило 3-2-1).
- Миграции запускают отдельным шагом развёртывания (job в Kubernetes, шаг пайплайна), а не при старте каждого экземпляра приложения, чтобы избежать гонок.