Миграции схемы БД

Материал из llmdocs — справочник разработчика
Портал: Базы данных
Миграции схемы БД
Назначениевоспроизводимое изменение схемы БД по версиям
Хранятсяв репозитории рядом с кодом
Подходыверсионный (императивный), декларативный (сравнение состояний)
PythonAlembic, 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:

  1. Расширить: добавить столбец full_name; код пишет в оба столбца.
  2. Перенести данные: заполнить full_name для старых строк пачками, чтобы не держать долгую транзакцию.
  3. Переключить: код читает только full_name.
  4. Сжать: после того как старые версии кода выведены из работы, удалить name.

Каждый шаг совместим с соседними версиями приложения, и любой можно откатить.

Практики

  • Одна миграция — одно логическое изменение; уже применённые в общих средах миграции не редактируются, ошибки исправляются новой миграцией.
  • Миграции данных отделяют от миграций схемы и делают идемпотентными.
  • В CI проверяют, что миграции применяются на чистой базе и что модели совпадают со схемой (alembic check, makemigrations --check); линтеры вроде squawk предупреждают об опасных операциях.
  • Перед рискованной миграцией — свежий бэкап и проверенное восстановление (см. Бэкапы: правило 3-2-1).
  • Миграции запускают отдельным шагом развёртывания (job в Kubernetes, шаг пайплайна), а не при старте каждого экземпляра приложения, чтобы избежать гонок.

См. также

Источники

  1. Alembic documentation
  2. Django — Migrations
  3. Flyway documentation
  4. Liquibase documentation
  5. Atlas — Declarative schema migrations
  6. Martin Fowler — Evolutionary Database Design
  7. PostgreSQL — ALTER TABLE (locking notes)