Модуль 10

Міграції та керування схемою БД

Ось готовий урок, створений у стилі Девіда Малана (CS50). Вмикаймо енергію, уяву та розбираймося з базами даних!


🎓 Тема: Міграції та керування схемою БД

(Або: Як змінювати фундамент будинку, не руйнуючи стін)


1. 🔥 Вступ: Проблема та мотивація

Уявіть, що ми з вами будуємо не просто програму, а хмарочос.

На першому поверсі (у першій версії програми) ми вирішили, що у мешканців (користувачів) будуть тільки Ім'я та Email. Ми залили бетон, звели стіни (створили таблицю в базі даних), запустили людей жити. Все чудово.

Але проходить місяць, і бізнес каже: "Нам терміново потрібно знати номер телефону кожного мешканця!".

І ось тут виникає проблема. У коді ви легко допишете поле phoneNumber у класі користувача. Це як намалювати нові двері на кресленні. Але реальна будівля (База Даних) вже стоїть! Там вже є бетонні комірки, де немає місця для телефону.

Риторичне запитання: Що станеться, якщо ви оновите код на сервері (де програма чекає телефон), а базу даних залишите старою? Правильно: Boom! 💥 Помилка 500. Програма впаде, бо намагатиметься записати дані в колонку, якої фізично не існує.

Раніше розробники дзвонили адмінам і казали: "Гей, Петре, зайди в консоль і виконай цей SQL-скрипт руками, поки сайт лежить". Це страшно. Це ненадійно.

Тут на сцену виходять Міграції. Це спосіб змінювати структуру бази даних (схему) в коді, автоматично, безпечно і так, щоб це можна було відстежити. Без них сучасна розробка — це як будівництво без каски: до першої цеглини на голову.


2. 🧠 Теоретична база (без сухої академічності)

Що таке Міграція? Простими словами: це файл, у якому записана історія змін вашої бази даних.

Уявіть це як Git, але для структури таблиць. * Коміт у Git каже: "Я змінив функцію логіну". * Міграція каже: "Я додав таблицю Users" або "Я додав колонку phone".

Як це працює "під капотом"?

Система міграцій (неважливо, чи це Django, TypeORM, Alembic чи Entity Framework) завжди має дві ключові речі:

  1. Папка з файлами міграцій. Кожен файл має часову мітку (timestamp), щоб система знала порядок виконання. Наприклад: 20231025_add_phone_to_users.sql.
  2. Службова таблиця в самій БД (зазвичай називається __migrations або schema_migrations).

Алгоритм такий: 1. Ви запускаєте команду migrate. 2. Програма дивиться в папку: "Ага, тут є 10 файлів". 3. Програма дивиться в таблицю в БД: "А тут записано, що ми виконали тільки 9". 4. Вона бере 10-й файл і виконує його. 5. Записує в таблицю: "Файл №10 виконано".

🔑 Два поняття, які треба запам'ятати:

Кожна міграція зазвичай складається з двох методів (інструкцій):

  • UP (Вгору): Що ми хочемо зробити? (Наприклад: CREATE TABLE).
  • DOWN (Вниз): Як це скасувати, якщо щось піде не так? (Наприклад: DROP TABLE).

Інтуїтивно: UP — це зібрати шафу з IKEA. DOWN — це розібрати її назад у коробку, не поламавши дошки.


3. 🧪 Приклади (від простого до реального)

Давайте подивимось на це очима розробника. Я буду використовувати псевдокод, схожий на SQL та JavaScript, щоб було зрозуміло всім.

Приклад 1: Народження (Init)

Ми створюємо таблицю товарів.

Чого ви очікуєте? Простої інструкції створення.

// Міграція: 001_create_products_table

up: {
    CREATE TABLE products (
        id SERIAL PRIMARY KEY,
        name VARCHAR(255),
        price INTEGER
    );
}

down: {
    DROP TABLE products; // Якщо треба все скасувати
}

Результат: У базі з'явилася таблиця.


Приклад 2: Еволюція (Alter)

Бізнес каже: "Товари мають бути активними або ні". Треба додати is_active.

Питання: Чи можемо ми просто відредагувати файл 001? Відповідь: НІ! 🙅‍♂️ Той файл вже виконано на проді. Якщо ви його зміните, історія порушиться. Ми створюємо новий файл.

// Міграція: 002_add_is_active_to_products

up: {
    ALTER TABLE products
    ADD COLUMN is_active BOOLEAN DEFAULT true;
}

down: {
    ALTER TABLE products
    DROP COLUMN is_active;
}

Чому DEFAULT true? Бо у нас вже є товари в базі! Якщо ми не дамо дефолтне значення, старі записи матимуть NULL, і код може зламатися.


Приклад 3: Вищий пілотаж (Data Migration)

Це вже рівень Senior. У нас є колонка fullname ("Ivan Petrenko"), а ми хочемо розділити її на first_name та last_name.

Це не просто зміна структури, це трансформація даних.

// Міграція: 003_split_names

up: {
    // 1. Створюємо нові колонки
    ALTER TABLE users ADD COLUMN first_name VARCHAR;
    ALTER TABLE users ADD COLUMN last_name VARCHAR;

    // 2. Магія перенесення даних (SQL Script)
    UPDATE users
    SET
      first_name = SPLIT_PART(fullname, ' ', 1),
      last_name  = SPLIT_PART(fullname, ' ', 2);

    // 3. (Опційно) Видаляємо стару
    // ALTER TABLE users DROP COLUMN fullname; -- Це небезпечно, краще в наступній міграції!
}

down: {
    // Скасування — це склеювання назад
    UPDATE users SET fullname = first_name || ' ' || last_name;
    ALTER TABLE users DROP COLUMN first_name;
    ALTER TABLE users DROP COLUMN last_name;
}

4. 🛠 Практична частина

Час "забруднити руки". Уявіть, що ви працюєте над backend-ом для інтернет-магазину кросівок.

Завдання 1: Створити базу (Foundation) Напишіть (на папері або в редакторі) UP та DOWN для таблиці orders (замовлення). * Поля: id, user_id, total_price, created_at.

Завдання 2: Зміна вимог (The Change) Менеджер забув, що треба зберігати статус замовлення ("нове", "в дорозі", "доставлено"). Напишіть нову міграцію, яка додає колонку status типу текстовий рядок.

Завдання 3: Робота над помилками (The Oops) Ви випадково назвали колонку total_price як total_prcie (одруківка) у першій міграції. * Варіант А: Міграція ще не пішла на продакшн (ви тільки у себе локально). Що робите? * Варіант Б: Міграція вже на продакшні. Що робите? (Це питання з зірочкою!)

Завдання 4: Міні-кейс Вам треба додати колонку passport_number для користувачів, але це поле має бути унікальним (UNIQUE). Але в базі вже є 5 користувачів без паспортів. Якщо ви додасте UNIQUE, база видасть помилку, бо NULL може конфліктувати (залежно від БД) або ви не зможете зробити колонку NOT NULL. Як ви напишете міграцію, щоб вона не впала?


5. 💡 Мислення як у розробника

Як думає новачок:

"Мені треба додати колонку. Я зайду в pgAdmin/DBeaver і клікну 'Add Column'. Готово, працює!" (Потім деплоїть код, і сервер падає, бо там колонки немає).

Як думає Senior:

"База даних — це стан. Стан треба версіонувати. Кожна зміна — це міграція. Перш ніж писати UP, я подумаю, як це вплине на 1 мільйон записів. Чи не заблокує це базу на 10 хвилин? І обов'язково перевірю DOWN на локальній машині."

⚠️ Типові помилки (Don't do this):

  1. Змінювати файл міграції після того, як його закомітили. Якщо файл полетів у репозиторій — він "священний". Треба правки? Створюй нову міграцію.
  2. Видаляти дані без бекапу. DROP TABLE або DROP COLUMN — це квиток в один кінець. Завжди думайте тричі.
  3. Логіка програми в міграціях. Не імпортуйте моделі/класи коду в файл міграції. Код змінюється, а міграції — це історія. Використовуйте чистий SQL або спеціальні методи мігратора.

6. 🧩 Підсумок

Отже, що ми маємо? Ми перестали боятися змінювати базу даних. Ми зрозуміли, що схема БД — це живий організм, який росте разом із кодом.

Тепер ви вмієте: 1. Створювати історію змін БД. 2. Безпечно додавати нові фічі. 3. Відкочувати зміни, якщо все пішло шкереберть (DOWN).

Що далі? Тепер, коли у нас є ідеальна структура таблиць і ми наповнили їх даними... як знайти потрібну інформацію серед мільйона рядків за 0.01 секунди? На наступному уроці ми поговоримо про магію швидкості — Індекси. 🚀

Це був CS50. Побачимось!