Міграції — це “паспорт” вашої бази даних у коді. Вони не просто створюють таблиці, вони фіксують правила, які тримають дані живими роками: що з чим пов’язано, які поля можуть бути порожні, які значення мають бути за замовчуванням і як швидко має працювати пошук. Якщо міграції зроблені погано, проєкт починає накопичувати сміття в БД, а будь-які списки та фільтри стають повільними.
Перший принцип: міграція описує структуру так, ніби ти пояснюєш її іншому розробнику без слів. Назва таблиці, назви полів, типи, зв’язки і індекси мають прямо відображати сенс. Новачки часто роблять схему “аби працювало”, але через місяць вона стає пасткою: немає цілісності, все nullable, foreign keys відсутні, індексів немає — і будь-яка аналітика перетворюється на біль.
Почнемо з nullable. Nullable означає, що поле може бути NULL, тобто “значення немає”. Це має бути свідоме рішення. Якщо поле обов’язкове для існування запису (наприклад, title у статті або user_id автора), воно не повинно бути nullable. Якщо поле може з’явитися пізніше або інколи не має сенсу (наприклад, published_at для чернетки, deleted_at для soft delete, main_img_author якщо не вказали автора фото), тоді nullable виправдане.
Головна помилка з nullable — робити nullable “про всяк випадок”. Так ти отримуєш купу записів, де ключові поля порожні, і потім ти починаєш писати десятки умов “якщо поле не порожнє”. Це ускладнює код, вбиває якість даних і часто створює баги на рівні логіки. Правильна дисципліна така: nullable — тільки там, де бізнес-сенс дозволяє відсутність значення.
Далі дефолти (default). Default — це значення за замовчуванням, яке база поставить, якщо ти не передав поле. Це важливо для стабільності. Наприклад, для статусу (draft), для boolean-полів (is_active = true/false), для лічильників (views = 0). Дефолт зменшує кількість “порожніх” випадків і робить поведінку передбачуваною навіть якщо в коді щось не передали.
Але дефолти теж треба ставити з розумом. Не треба ставити default там, де значення має визначатися логікою сценарію. Наприклад, published_at не має дефолту “now”, бо публікація — це дія, а не автоматична подія. user_id теж не має дефолту, бо це прив’язка до конкретного користувача. Дефолт — для стабільних “початкових” станів, а не для важливих бізнес-рішень.
Тепер foreign keys. Foreign key — це зовнішній ключ, який зв’язує таблиці і змушує базу даних тримати цілісність. Простий приклад: якщо в таблиці news є user_id, foreign key гарантує, що цей user_id реально існує в таблиці users. Без цього ти можеш випадково записати user_id = 999999, і база це проковтне. Потім ти будеш ловити “null автора” або падіння на зв’язках.
Foreign keys важливі не лише для “красоти”, а для запобігання системним поломкам. Вони роблять дані самозахищеними: ти не зможеш видалити користувача, якщо на нього посилаються записи (або зможеш, але з контрольованою поведінкою). Це підводить нас до правила on delete: що робити з дочірніми записами, якщо видалили батьківський.
Є кілька типових стратегій. cascade означає: видалили батька — видали і дітей. Це доречно, наприклад, для допоміжних таблиць, які не мають сенсу без батьківського запису. restrict або no action означає: не давай видалити батька, якщо є діти. Це доречно для важливих даних, де видалення не має бути випадковим. set null означає: якщо батька видалили, в дочірньому полі став NULL. Це працює лише якщо поле nullable і якщо бізнес-сенс дозволяє “без батька”.
Окрема дисципліна — узгодженість типів. Якщо users.id — big integer, то і news.user_id має бути таким самим типом. У Laravel сучасний стандарт — foreignId() або foreignIdFor() і constrained(), бо це зменшує ризик помилок і робить код міграцій коротшим і яснішим. Новачкам важливо звикнути саме до цього стилю.
Тепер індекси. Індекс — це те, що робить списки і фільтри швидкими. Якщо таблиця маленька, різниця непомітна. Але в медіапроєкті, де ти маєш десятки тисяч новин, індекси визначають, чи буде адмінка “літати”, чи “думати” по 5 секунд на кожен фільтр. Тому індекси не “оптимізація на потім”, а частина дизайну схеми.
Які поля найчастіше потребують індексів. Перше — всі foreign keys, бо по них йдуть JOIN і фільтри (user_id, section_id, resource_id). Друге — поля, по яких часто фільтрують: статуси, дати, slug, унікальні коди. Третє — поля, по яких часто сортують: published_at, created_at, інколи id як сурогатний порядок. Четверте — комбінації полів, якщо фільтр завжди йде “разом” (наприклад, resource_id + published_at).
Тут важлива правильна думка: індекс — під запит, а не “на всяк випадок”. Якщо у вас є сторінка “контроль публікацій” з фільтрами по даті, користувачу і статусу — індекси мають відображати саме це. Інакше ви отримаєте повільні запити або повний скан таблиці. Але також не можна “наіндексувати все”, бо це збільшує розмір БД і уповільнює вставки/оновлення.
Складені індекси (composite) — коли індексуються кілька полів разом. Вони корисні, якщо ти майже завжди шукаєш по двох полях одночасно. Порядок полів в такому індексі має значення: база добре використовує індекс, коли фільтр починається з першого поля індексу. Тому індекси проектують не “як красиво”, а під реальні сценарії: які фільтри йдуть першими, які — додатковими.
Ще один критичний момент — унікальні індекси. Якщо поле повинно бути унікальним (slug, email, code), це треба фіксувати на рівні бази, а не тільки валідацією. Бо валідацію можна обійти або можна зловити гонку двох запитів. База — це остання лінія оборони. Унікальний індекс гарантує, що дубль фізично не запишеться.
Як зрозуміти, що міграції спроєктовані добре. Коли ти дивишся на таблицю, ти одразу бачиш: які поля обов’язкові, які можуть бути порожні, які мають дефолти, як таблиця пов’язана з іншими, і які індекси підтримують реальні списки/фільтри. Коли ти робиш новий модуль, тобі не треба “вигадувати” — ти додаєш за стандартом: foreign keys, індекси на фільтри, дефолти на статуси, nullable тільки по сенсу.
Практичний порядок роботи при додаванні нової таблиці завжди такий. Спочатку визначаєш сутність і поля: які обов’язкові. Потім визначаєш зв’язки: хто батько, хто дитина, що робити при видаленні. Потім визначаєш типові запити: як буде виглядати список, фільтри, сортування. І лише потім ставиш індекси під ці запити. Останнім кроком ставиш дефолти для стабільних станів і перевіряєш, чи nullable там, де треба, а не “де зручно”.
Висновок: міграції — це інженерна дисципліна, яка визначає стабільність і швидкість проєкту. Nullable — тільки там, де дозволяє сенс, дефолти — для початкових станів, foreign keys — для цілісності, індекси — для реальних запитів і фільтрів. Якщо ти закладеш ці правила з першого місяця, твій Laravel-проєкт не “посиплеться” на масштабі і не перетвориться на базу з випадковими даними.