Ответ
Миграция базы данных — это управляемое, версионное изменение схемы БД (таблицы, столбцы, индексы, ограничения), которое позволяет эволюционировать структуре данных параллельно с развитием приложения, сохраняя при этом существующие данные.
Зачем это нужно:
- Добавление нового функционала, требующего новых таблиц или полей.
- Рефакторинг существующей схемы (переименование, изменение типа).
- Внесение исправлений (багфиксы) в структуру БД.
- Обеспечение идентичности схемы БД во всех окружениях (разработка, тестирование, продакшн).
Базовый пример на SQL:
-- Миграция '001_add_user_age.sql' (накат)
ALTER TABLE users ADD COLUMN age INTEGER;
-- Соответствующий откат (rollback)
ALTER TABLE users DROP COLUMN age;
Как это работает в контексте C++ проекта: На практике для C++ приложений миграции часто управляются внешними инструментами (Flyway, Liquibase) или скриптами. Процесс выглядит так:
- Версионность: Каждая миграция — это файл с SQL или кодом на C++, имеющий уникальный номер или метку времени (например,
20240515_01_add_email.sql). - Журнал применений: В БД создается служебная таблица (например,
schema_version), которая отслеживает, какие миграции уже применены. - Применение: Инструмент миграций сравнивает журнал с доступными файлами миграций и применяет недостающие в правильном порядке.
- Откат: Для каждой миграции часто пишется обратный 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 или самописные скрипты) и заставляют его делать грязную работу. Схема примерно такая:
-
Версионность — наше всё. Каждая миграция — это отдельный файлик.
20240515_01_добавили_почту.sql,20240515_02_исправили_косяк_с_телефоном.sql. По названию или номеру сразу ясно, в каком порядке их применять. Удивление пиздец, если порядок нарушить. -
Журнал — чтобы не охуеть. В самой базе заводится специальная стукач-таблица (чаще всего
schema_version). Её единственная задача — ябедничать и помнить, какие миграции уже выполнены, а какие нет. Без неё ты просто хуй с горы — не поймёшь, в каком состоянии твоя схема. -
Применение — дело техники. Инструмент миграций приходит, смотрит в эту стукач-таблицу, потом смотрит в папку с файлами-миграциями. Видит, что файл
003_...есть, а в журнале его нет. «Ага, — думает, — эту ещё не делали». И выполняет SQL из него. Всё чётко, по списку. -
Откат — страховка от пиздеца. К каждой миграции хорошо бы приложить «противоядие» — скрипт, который откатывает изменения назад. Мало ли, новая духовка взорвётся на второй день. Надо же иметь возможность вернуть старую плиту, пока ищем новую.
Как это всё в проекте валяется:
database/
├── migrations/ <-- Здесь живут все наши «планы перепланировки»
│ ├── 001_начальная_схема.sql
│ ├── 002_добавили_возраст_пользователям.sql
│ └── 003_индекс_на_почту_чтобы_быстрее_искать.sql
└── deploy_scripts/ <-- А здесь скрипт, который орёт «Бегите файлы по порядку!»
Главный принцип, без которого будет хиросима и нигерсраки:
Миграции должны быть идемпотентными. Это мудрёное слово означает простую вещь: сколько раз ни запускай один и тот же скрипт миграции — результат должен быть один. Он не должен на втором запуске орать «Бля, это поле уже есть, че ты мне суёшь!?» и падать.
Достигается это простыми трюками в SQL:
-- Вместо голого CREATE TABLE
CREATE TABLE IF NOT EXISTS users (...);
-- Вместо простого ADD COLUMN (что упадёт при повторе)
-- ...проверяем, нет ли уже такого столбца (способы зависят от СУБД).
Короче, нужно э бошка думай и писать миграции так, чтобы их можно было запустить сто раз, и чтобы после сотого раза в базе не образовалась пиздопроебибна.
Вот и вся магия. Кажется сложным, но без этого подхода рано или поздно наступает момент, когда у тебя на проде одна схема, у тебя на ноуте — вторая, а у тестировщика — третья, и вы три дня не можете понять, почему фича накрылась медным тазом только у одного из вас. А миграции этот бардак предотвращают.