Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Middle: питання на співбесіді з теми «Міграції»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

migrate:rollback відкочує останній «батч» міграцій, викликаючи їхні методи down():

php artisan migrate:rollback              # останній батч
php artisan migrate:rollback --step=1     # рівно одну міграцію

migrate:reset відкочує всі міграції по черзі. migrate:refresh - відкочує все й накатує заново. migrate:fresh - видаляє всі таблиці й накатує міграції з нуля, взагалі не заглядаючи в down().

Що з цього небезпечне в проді: усі чотири. Кожна знищує дані, а migrate:fresh робить це найшвидше й без шансу на down(). У проді припустимий лише php artisan migrate.

Laravel сам питає підтвердження в продакшн-середовищі, і саме тому --force не варто вписувати в скрипти «щоб не заважало».

Практика, що рятує:

  • Перевіряйте відкат локально одразу після написання: migrate → migrate:rollback --step=1 → migrate. Половина міграцій має неробочий down(), і виявляється це в найгірший момент.
  • Пишіть down() чесно, а якщо відкат неможливий - хай кидає виняток, це краще за мовчазну порожню реалізацію.
  • Не редагуйте вже застосовану міграцію - додавайте нову. Виправлений файл не перезастосується там, де він уже відпрацював.
  • Видалення колонки й перейменування - окремими релізами від коду, що їх читає, інакше деплой ламає працюючий застосунок у проміжку.

Докладніше в документації: Міграції

На таблиці в кілька мільйонів рядків необережна міграція блокує запис на хвилини, і застосунок лежить.

Що блокує довго:

  • Додавання колонки NOT NULL зі значенням за замовчуванням у старих версіях MySQL переписує таблицю цілком. У MySQL 8 і PostgreSQL 11+ це вже метадані, але перевіряти варто саме свою версію.
  • Створення індексу звичайним CREATE INDEX тримає таблицю на час побудови.
  • Зміна типу колонки майже завжди означає перезапис.

Безпечний порядок для нової колонки:

// 1. Спершу nullable - миттєво, без перезапису.
Schema::table('vacancies', function (Blueprint $table) {
    $table->string('short_name')->nullable()->after('title');
});

Далі заповнити дані пачками - окремою командою, не в міграції:

Vacancy::whereNull('short_name')->chunkById(500, function ($vacancies) {
    foreach ($vacancies as $vacancy) {
        $vacancy->update(['short_name' => Str::limit($vacancy->title, 40, '')]);
    }
});

І лише потім, якщо потрібно, робити колонку обовʼязковою - окремою міграцією, коли даних без значення вже немає.

Індекс без блокування:

// PostgreSQL
DB::statement('CREATE INDEX CONCURRENTLY vacancies_level_index ON vacancies (level)');

CONCURRENTLY не можна виконувати всередині транзакції, тож у міграції потрібно вимкнути обгортання:

public $withinTransaction = false;

Загальне правило: розділяйте зміну схеми й заповнення даних. Міграція має бути швидкою операцією над структурою, а перенесення даних - командою, яку можна запустити, зупинити й продовжити.

Докладніше в документації: Міграції

Кожен запуск php artisan migrate записує виконані міграції в таблицю migrations з одним номером пакета (batch).

php artisan migrate:rollback            # відкотити останній пакет цілком
php artisan migrate:rollback --step=1   # рівно одну останню міграцію
php artisan migrate:rollback --batch=3  # конкретний пакет
php artisan migrate:status              # що виконано і в якому пакеті

Відкат викликає down(), тож вона має справді повертати попередній стан: видалити створену колонку, відновити змінений тип.

Інші команди - і чим вони небезпечні:

  • migrate:reset - відкотити все;
  • migrate:refresh - відкотити все й виконати знову (проходить усі down());
  • migrate:fresh - видалити всі таблиці й виконати міграції з нуля, down() не викликається.

Останні три знищують дані - на продакшені їх не запускають, а локально з копією прод-бази - теж обережно.

На продакшені відкат міграції, яка видалила колонку з даними, не поверне дані. Тому руйнівні зміни роблять окремим релізом пізніше, а відкат релізу - новою міграцією вперед, а не rollback.

Докладніше в документації: Відкат міграцій

Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('post_id')->constrained()->cascadeOnDelete();
    $table->foreignIdFor(User::class)->nullable()->constrained()->nullOnDelete();
    $table->timestamps();
});
  • foreignId('post_id')->constrained() - колонка unsignedBigInteger плюс обмеження на posts.id (таблицю Laravel виводить з назви колонки);
  • foreignIdFor(User::class) - назву колонки бере з моделі.

Що буде з дітьми при видаленні батька - вирішують явно:

  • cascadeOnDelete() - видалити разом (коментарі поста);
  • nullOnDelete() - обнулити посилання (автор видалив акаунт, коментар лишається анонімним; колонка має бути nullable);
  • restrictOnDelete() - заборонити видалення, поки є діти (клієнт із рахунками).

Що варто знати:

  • обмеження працює на рівні бази й обходить Eloquent - події моделей для каскадно видалених рядків не спрацюють;
  • м'яке видалення батька каскад не запускає - це звичайний UPDATE;
  • MySQL сам створює індекс для зовнішнього ключа, PostgreSQL - ні, тож там індекс на post_id додають окремо;
  • видалити ключ: $table->dropForeign(['post_id']).

Докладніше в документації: Обмеження зовнішніх ключів