Что такое миграция базы данных?

Ответ

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

Зачем это нужно:

  • Добавление нового функционала, требующего новых таблиц или полей.
  • Рефакторинг существующей схемы (переименование, изменение типа).
  • Внесение исправлений (багфиксы) в структуру БД.
  • Обеспечение идентичности схемы БД во всех окружениях (разработка, тестирование, продакшн).

Базовый пример на SQL:

-- Миграция '001_add_user_age.sql' (накат)
ALTER TABLE users ADD COLUMN age INTEGER;

-- Соответствующий откат (rollback)
ALTER TABLE users DROP COLUMN age;

Как это работает в контексте C++ проекта: На практике для C++ приложений миграции часто управляются внешними инструментами (Flyway, Liquibase) или скриптами. Процесс выглядит так:

  1. Версионность: Каждая миграция — это файл с SQL или кодом на C++, имеющий уникальный номер или метку времени (например, 20240515_01_add_email.sql).
  2. Журнал применений: В БД создается служебная таблица (например, schema_version), которая отслеживает, какие миграции уже применены.
  3. Применение: Инструмент миграций сравнивает журнал с доступными файлами миграций и применяет недостающие в правильном порядке.
  4. Откат: Для каждой миграции часто пишется обратный SQL-скрипт, позволяющий отменить изменения при необходимости.

Пример организации в проекте:

database/
├── migrations/
│   ├── 001_initial_schema.sql
│   ├── 002_add_user_age.sql
│   └── 003_create_index_on_email.sql
└── deploy_scripts/ (запускает миграции в правильном порядке)

Ключевой принцип: Миграции должны быть идемпотентными — их повторное применение не должно ломать схему. Это часто достигается использованием конструкций вроде CREATE TABLE IF NOT EXISTS или проверкой существования столбца перед ALTER TABLE.

Ответ 18+ 🔞

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

Зачем весь этот цирк? Ну, например, жена говорит: «Хочу новую встроенную духовку на кухню». Это — новый функционал. Тебе нужно аккуратно вырвать старую плиту, не сломав кафель, и впихнуть новую, подключив её к газу и электричеству. Если ты просто возьмёшь кувалду и начнёшь всё крушить, вечером будет пиздец, а не ужин. Так и с БД: нужно менять структуру, не угробив при этом живые данные.

Или другой случай: ты переименовал комнату «кабинет» в «комнату для игр в приставку». Это рефакторинг. Если почтальон будет искать «кабинет», чтобы отдать повестку, он её не найдёт. Надо всем чётко сказать: «Мужик, теперь это вот так называется, запомни». Чтобы и на продакшене (в реальной квартире), и у тебя на ноуте (в окружении разработки) все названия были одинаковые, а не манда с ушами.

Смотри, как это выглядит в коде, на простом примере:

-- Это мы добавляем новую фичу — столбец «возраст» к таблице пользователей.
-- Файл можно назвать, например, '001_ой_все_стареют.sql'
ALTER TABLE users ADD COLUMN age INTEGER;

-- А это — наш план отступления на случай, если всё пошло по пизде.
-- Откат, отмена, «ой, мамочка, давайте как было».
ALTER TABLE users DROP COLUMN age;

Как этот бардак организовать в нормальном C++ проекте?

Честно говоря, сам C++ этим обычно не парится — это не его епархия. Чаще всего берут какой-нибудь готовый инструмент (типа Flyway или самописные скрипты) и заставляют его делать грязную работу. Схема примерно такая:

  1. Версионность — наше всё. Каждая миграция — это отдельный файлик. 20240515_01_добавили_почту.sql, 20240515_02_исправили_косяк_с_телефоном.sql. По названию или номеру сразу ясно, в каком порядке их применять. Удивление пиздец, если порядок нарушить.

  2. Журнал — чтобы не охуеть. В самой базе заводится специальная стукач-таблица (чаще всего schema_version). Её единственная задача — ябедничать и помнить, какие миграции уже выполнены, а какие нет. Без неё ты просто хуй с горы — не поймёшь, в каком состоянии твоя схема.

  3. Применение — дело техники. Инструмент миграций приходит, смотрит в эту стукач-таблицу, потом смотрит в папку с файлами-миграциями. Видит, что файл 003_... есть, а в журнале его нет. «Ага, — думает, — эту ещё не делали». И выполняет SQL из него. Всё чётко, по списку.

  4. Откат — страховка от пиздеца. К каждой миграции хорошо бы приложить «противоядие» — скрипт, который откатывает изменения назад. Мало ли, новая духовка взорвётся на второй день. Надо же иметь возможность вернуть старую плиту, пока ищем новую.

Как это всё в проекте валяется:

database/
├── migrations/          <-- Здесь живут все наши «планы перепланировки»
│   ├── 001_начальная_схема.sql
│   ├── 002_добавили_возраст_пользователям.sql
│   └── 003_индекс_на_почту_чтобы_быстрее_искать.sql
└── deploy_scripts/      <-- А здесь скрипт, который орёт «Бегите файлы по порядку!»

Главный принцип, без которого будет хиросима и нигерсраки:

Миграции должны быть идемпотентными. Это мудрёное слово означает простую вещь: сколько раз ни запускай один и тот же скрипт миграции — результат должен быть один. Он не должен на втором запуске орать «Бля, это поле уже есть, че ты мне суёшь!?» и падать.

Достигается это простыми трюками в SQL:

-- Вместо голого CREATE TABLE
CREATE TABLE IF NOT EXISTS users (...);

-- Вместо простого ADD COLUMN (что упадёт при повторе)
-- ...проверяем, нет ли уже такого столбца (способы зависят от СУБД).

Короче, нужно э бошка думай и писать миграции так, чтобы их можно было запустить сто раз, и чтобы после сотого раза в базе не образовалась пиздопроебибна.

Вот и вся магия. Кажется сложным, но без этого подхода рано или поздно наступает момент, когда у тебя на проде одна схема, у тебя на ноуте — вторая, а у тестировщика — третья, и вы три дня не можете понять, почему фича накрылась медным тазом только у одного из вас. А миграции этот бардак предотвращают.