SQL и Qt

Миграции схемы БД: инструменты и практики (Flyway, Liquibase, Alembic)

Введение

Схема БД развивается во времени вместе с приложением (статья 402, заключительная оговорка) — миграции дают систематический, версионируемый способ применять и отслеживать изменения схемы согласованно между разными окружениями (разработка, тестирование, production) и разными членами команды, вместо ручного, неотслеживаемого выполнения произвольных SQL-команд напрямую.

Концепция

Инструмент миграций хранит последовательность пронумерованных/датированных файлов миграции (каждый описывающий одно конкретное изменение схемы — создание таблицы, добавление столбца, создание индекса) и отдельную таблицу метаданных внутри самой БД, отслеживающую, какие миграции уже были применены к конкретной БД — это позволяет автоматически, безопасно и в правильном порядке применить только недостающие, ещё не применённые миграции при развёртывании на любом окружении, без необходимости вручную помнить и выполнять каждое отдельное изменение схемы.

Пример кода: Миграции схемы БД

-- Flyway — типичная структура файла миграции (именование по соглашению: V<номер>__<описание>.sql)
-- Файл: V1__create_customers_table.sql
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE
);

-- Файл: V2__add_registered_at_to_customers.sql
ALTER TABLE customers ADD COLUMN registered_at DATE DEFAULT CURRENT_DATE;

-- Файл: V3__create_orders_table.sql
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_id INTEGER REFERENCES customers(id),
    total_amount DECIMAL(12, 2)
);
# Применение миграций через Flyway CLI — выполняет ВСЕ ещё не применённые миграции по порядку
flyway -url=jdbc:postgresql://localhost/mydb -user=admin migrate

# Проверка текущего состояния — какие миграции уже применены
flyway -url=jdbc:postgresql://localhost/mydb -user=admin info
# Alembic (Python, часто используется с SQLAlchemy) — пример файла миграции
# generated by alembic, упрощённый пример
def upgrade():
    op.create_table(
        'customers',
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('name', sa.String(100), nullable=False)
    )

def downgrade():
    op.drop_table('customers') # явный путь ОТКАТА миграции — не все инструменты это требуют

Пояснения к коду

Последовательность файлов V1__, V2__, V3__ показывает основной принцип Flyway-подобных систем — каждое изменение схемы фиксируется как отдельный, неизменяемый после применения файл с явным порядковым номером, и инструмент миграций отслеживает (через специальную таблицу внутри самой БД), какие из этих файлов уже были применены к конкретному экземпляру БД, применяя при следующем запуске лишь недостающие. Alembic показывает альтернативный, более процедурный (на Python, а не на чистом SQL) подход к описанию миграций, с явным разделением upgrade()/downgrade() — последняя функция описывает, как откатить именно это конкретное изменение, что не все инструменты миграций требуют одинаково строго (Flyway, например, по умолчанию не настроен на автоматический откат отдельных миграций таким явным образом).

Подводные камни

  • Ручное изменение схемы БД production-окружения напрямую, минуя систему миграций, «просто чтобы быстро что-то поправить» — это нарушает согласованность между тем, что отслеживает система миграций как «применённое», и реальным состоянием production-схемы, что может привести к конфликтам и ошибкам при последующем применении новых, легитимных миграций, ожидающих определённого, отслеживаемого состояния схемы, отличного от того, что реально присутствует после неотслеживаемого ручного изменения.
  • Изменение уже применённого, существующего файла миграции вместо создания нового файла с дальнейшим изменением — большинство инструментов миграций (включая Flyway) вычисляют контрольную сумму содержимого каждого файла миграции и обнаруживают (с ошибкой) попытку повторного применения уже применённой миграции, содержимое которой изменилось после первого применения — это защитный механизм, предотвращающий рассогласование между разными окружениями, где одна и та же «версия» миграции могла бы означать разные реальные изменения схемы в зависимости от того, когда именно она была применена.
  • Отсутствие версионирования файлов миграции в системе контроля версий вместе с остальным кодом приложения — миграции схемы БД логически являются частью кодовой базы проекта (поскольку код приложения часто зависит от конкретной структуры схемы, которую эти миграции создают), и хранение их отдельно от обычного процесса контроля версий (Git) теряет историю изменений и усложняет согласованное развёртывание соответствующих друг другу версий кода приложения и схемы БД.
  • Игнорирование необходимости тестирования миграций на копии реальных, представительных по объёму production-данных перед применением к самой production-БД — миграция, успешно протестированная только на пустой или очень маленькой тестовой БД, может вести себя совершенно иначе на действительно больших объёмах production-данных (например, добавление столбца с вычислением значения по умолчанию для каждой существующей строки может занять неожиданно много времени и заблокировать таблицу на этот период, статья 413, для таблицы с миллионами строк), и для критичных production-систем стоит явно протестировать миграции на представительной по объёму копии данных перед применением к реальной production-БД.