Когда схема базы данных тоже требует контроля версий

Представьте: у вашей команды отлаженный CI/CD пайплайн для кода приложения. Каждый pull request запускает автоматические тесты, сборку контейнера и деплой на стейджинг. Затем наступает деплой в продакшен. Пайплайн выполняется, приложение стартует — и сразу падает с ошибкой "column not found". Кто-то забыл запустить миграцию БД, добавляющую колонку phone_number. Деплой провален, пользователи видят ошибки, команда в панике выясняет, что пошло не так.

Этот сценарий повторяется в командах ежедневно. Код приложения версионируется, тестируется и разворачивается через пайплайн. А изменения схемы базы данных остаются на потом — что-то, что кто-то запускает вручную до или после деплоя. Разрыв между деплоем кода и изменениями схемы создаёт брешь, в которую проскальзывают ошибки.

Проблема: откуда пайплайн знает, что уже выполнено?

Когда у вас есть директория со скриптами миграций вроде V001_create_users.sql, V002_add_phone.sql и V003_add_index.sql, пайплайну нужно знать, какие из них уже применены к базе. Нельзя каждый раз при деплое запускать все файлы с начала. В продакшене уже есть реальные данные. Повторный запуск V001 либо упадёт, потому что таблица уже существует, либо, что хуже, удалит и пересоздаст таблицы, уничтожив данные клиентов.

Без механизма отслеживания команды прибегают к ручным проверкам. Кто-то заходит в базу, выполняет \dt или SHOW TABLES и пытается вспомнить, что деплоили на прошлой неделе. Или полагаются на общую таблицу в Google Docs, которую никто не обновляет. Или просто запускают миграцию и надеются на лучшее.

Ни один из этих подходов не масштабируется. Они вносят человеческий фактор, замедляют деплой и порождают страх перед любыми изменениями в базе.

Решение: таблица миграций в базе данных

Ответ удивительно прост: пусть база данных сама отслеживает историю своих миграций. Создайте специальную таблицу, обычно называемую schema_migrations или migration_history, которая записывает каждый выполненный скрипт миграции.

Вот как это работает на практике:

  1. При первом запуске инструмента миграций на пустой базе создаётся таблица миграций.
  2. После успешного выполнения каждого скрипта инструмент вставляет строку с именем скрипта и временной меткой.
  3. При последующих деплоях пайплайн читает таблицу миграций, сравнивает её со списком доступных файлов миграций и запускает только те скрипты, которых ещё нет в таблице.

Например, после выполнения V001_create_users.sql таблица миграций содержит одну строку: V001_create_users.sql. Когда следующий деплой включает V002_add_phone.sql, пайплайн проверяет таблицу, видит, что V002 отсутствует, и запускает его. После успеха добавляется новая строка. Сама база данных становится единственным источником истины о том, какая версия схемы сейчас работает.

Почему это важно для вашего пайплайна

Этот механизм называется блокировкой версий (version locking). База данных хранит авторитетную запись о собственном состоянии. Не нужен отдельный конфигурационный файл, переменная окружения, которая может рассинхронизироваться, или ручной чек-лист, который кто-то забудет обновить.

Для CI/CD пайплайна это критически важно. Пайплайн теперь может принять объективное решение: "Основываясь на том, что говорит мне база, я должен запустить эти три файла миграции". Никаких догадок, ручных проверок, страха запустить одну и ту же миграцию дважды.

Следующая диаграмма последовательности иллюстрирует этот точный поток:

sequenceDiagram participant Pipeline as CI/CD Pipeline participant Scripts as Migration Scripts Directory participant Table as Database Migration Table participant Schema as Database Schema Pipeline->>Table: Query applied migrations Table-->>Pipeline: Return list (e.g., V001, V002) Pipeline->>Scripts: Compare with available scripts Scripts-->>Pipeline: New scripts (e.g., V003) Pipeline->>Schema: Run V003_add_index.sql Schema-->>Pipeline: Success Pipeline->>Table: Insert V003 record Pipeline->>Schema: Application starts with updated schema

Разные инструменты миграций реализуют это немного по-своему. Некоторые записывают контрольную сумму каждого файла миграции, чтобы обнаружить, не изменил ли кто-то уже выполненный скрипт. Другие используют последовательные номера версий вместо имён файлов. Некоторые инструменты хранят историю миграций в отдельной схеме или базе данных. Но основной принцип остаётся неизменным: база данных отслеживает свою собственную историю, а пайплайн читает эту историю, чтобы определить следующие шаги.

Начальная загрузка: первая миграция

Здесь есть проблема курицы и яйца. Таблица миграций должна существовать до того, как в неё можно будет записать любую другую миграцию. Как её создать?

Большинство инструментов миграций делают это автоматически. При первом запуске инструмента на пустой базе он создаёт таблицу миграций как часть процесса начальной загрузки. Некоторые инструменты даже записывают это действие как первую запись в истории миграций.

Если вы внедряете скрипты миграций для существующей базы данных, в которой уже есть таблицы и данные, нужен другой подход. Здесь на помощь приходит базовая миграция (baseline migration). Вместо того чтобы пытаться воссоздать каждое историческое изменение, вы создаёте один скрипт миграции, который фиксирует текущее состояние схемы базы данных. Вы помечаете его как базовый, и инструмент миграций записывает его как уже применённый. С этого момента вы добавляете только новые скрипты миграций для будущих изменений.

Базовая миграция — прагматичное решение. Она признаёт, что вы не можете переписать историю, но можете начать отслеживать изменения с сегодняшнего дня. Альтернативой было бы восстановление каждого изменения схемы, когда-либо сделанного, что непрактично для большинства команд.

Что таблица миграций не решает

Таблица миграций решает одну конкретную проблему: знание того, какие скрипты уже были выполнены. Она даёт пайплайну надёжный способ определить текущую версию схемы и применить ожидающие изменения.

Но она не решает всего. Таблица миграций хорошо работает для аддитивных изменений: добавление таблиц, колонок или индексов. Эти изменения не ломают существующий код приложения, написанный для старой схемы. Проблемы начинаются, когда нужно удалить или переименовать колонки, изменить типы данных или реструктурировать таблицы. Такие деструктивные или трансформирующие изменения могут сломать работающее приложение, вызвать простой или повредить данные.

Таблица миграций говорит вам, что было выполнено, но не говорит, совместимо ли работающее приложение с новой схемой. Для этого требуется другой набор практик: обратно совместимые миграции, поэтапные развёртывания и тщательная координация между деплоем кода и изменениями схемы.

Практический чек-лист для отслеживания миграций

  • Выберите инструмент миграций, поддерживающий автоматическое отслеживание через таблицу миграций.
  • Убедитесь, что таблица миграций создаётся во время первого деплоя, а не вручную.
  • Никогда не изменяйте скрипт миграции, который уже был применён в продакшене.
  • Если внедряете миграции для существующей базы данных, сначала создайте базовую миграцию.
  • Включите выполнение миграций как шаг в ваш пайплайн деплоя, а не как ручной процесс.
  • Тестируйте миграции в стейджинге, который зеркалирует объём данных продакшена.

Конкретный вывод

Схема вашей базы данных так же важна, как и код приложения, и она заслуживает того же уровня контроля версий и автоматизации. Таблица миграций даёт вашему пайплайну надёжный, основанный на базе данных способ узнать, что уже применено, а что ещё нужно выполнить. Без неё вы гадаете. С ней у вас есть единый источник истины, который устраняет наиболее распространённую причину сбоев деплоя, связанных с изменениями базы данных. Начните отслеживать версии вашей схемы сегодня, и ваш будущий «я» скажет вам спасибо во время следующего деплоя.